├── .github ├── auto_assign.yml └── workflows │ ├── auto-assign-issue.yml │ ├── auto-assign-pr.yml │ └── buildAndTest.yml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── pre-push ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── FULLRESTORE.md ├── GENERATOR_DOC.md ├── LICENSE ├── README.md ├── __test__ ├── backup.test.ts ├── help.test.ts ├── pw.test.ts ├── sevenZip.test.ts └── sevenZipUpdateBinPath.test.ts ├── api ├── Global.d.ts ├── Joplin.d.ts ├── JoplinClipboard.d.ts ├── JoplinCommands.d.ts ├── JoplinContentScripts.d.ts ├── JoplinData.d.ts ├── JoplinFilters.d.ts ├── JoplinImaging.d.ts ├── JoplinInterop.d.ts ├── JoplinPlugins.d.ts ├── JoplinSettings.d.ts ├── JoplinViews.d.ts ├── JoplinViewsDialogs.d.ts ├── JoplinViewsMenuItems.d.ts ├── JoplinViewsMenus.d.ts ├── JoplinViewsNoteList.d.ts ├── JoplinViewsPanels.d.ts ├── JoplinViewsToolbarButtons.d.ts ├── JoplinWindow.d.ts ├── JoplinWorkspace.d.ts ├── index.ts ├── noteListType.d.ts ├── noteListType.ts └── types.ts ├── img ├── icon.svg ├── icon_256.png ├── icon_32.png ├── joplin_path_in_gui.jpg ├── main.png └── showcase1.png ├── package-lock.json ├── package.json ├── plugin.config.json ├── src ├── Backup.ts ├── helper.ts ├── index.ts ├── locales │ ├── de_DE.json │ ├── en_US.json │ ├── ro_MD.json │ ├── ro_RO.json │ ├── sk_SK.json │ └── zh_CN.json ├── manifest.json ├── settings.ts ├── sevenZip.ts └── webview.css ├── tsconfig.json └── webpack.config.js /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addAssignees: true 2 | addReviewers: false 3 | reviewers: 4 | - JackGruber 5 | assignees: 6 | - JackGruber 7 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-issue.yml: -------------------------------------------------------------------------------- 1 | name: Issue assignment 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Auto-assign issue" 12 | uses: pozil/auto-assign-issue@v1.4.0 13 | with: 14 | assignees: JackGruber 15 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-pr.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | workflow_dispatch: 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: kentaro-m/auto-assign-action@v2.0.0 12 | with: 13 | configuration-path: ".github/auto_assign.yml" 14 | -------------------------------------------------------------------------------- /.github/workflows/buildAndTest.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: [push, pull_request] 3 | jobs: 4 | buildAndTest: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-node@v4 9 | - name: Install dependencies 10 | run: npm install 11 | - name: Build 12 | run: npm run dist 13 | - name: Run test 14 | run: npm test 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | publish/ 4 | __test__/tests 5 | tools/*.js 6 | .env 7 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.md 2 | !README.md 3 | /*.jpl 4 | /api 5 | /src 6 | /dist 7 | tsconfig.json 8 | webpack.config.js 9 | .env 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | api/ 2 | dist/ 3 | publish/ 4 | webpack.config.js 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "endOfLine": "auto", 7 | "overrides": [ 8 | { 9 | "files": ["tsconfig.json"], 10 | "options": { 11 | "tabWidth": 4, 12 | "useTabs": true 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.insertFinalNewline": true, 3 | "editor.tabSize": 2 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## not released 4 | 5 | ## v1.4.4 (2025-05-05) 6 | 7 | - Add: Added Slovak language #89 8 | - Add: Translation option `settings.section.label` to translate the plugin setting section label 9 | - Update: German translation 10 | 11 | ## v1.4.3 (2025-04-27) 12 | 13 | - Add: ro_RO and ro_MD translations #88 14 | 15 | ## v1.4.2 (2024-07-17) 16 | 17 | - Fix: Some EN translations and typos #76 18 | 19 | ## v1.4.1 (2024-03-10) 20 | 21 | - Fix: Don't show `There is no Data to export` on empty profiles and automatic backups #71 22 | 23 | ## v1.4.0 (2024-02-22) 24 | 25 | - Changes that are required for the Joplin default plugin 26 | - Renamed Plugin from `Simple Backup` to `Backup` 27 | - Add: Allow creating of subfolders for each profile 28 | 29 | ## v1.3.6 (2024-01-11) 30 | 31 | - Add: Screenshots / icon for [https://joplinapp.org/plugins/](https://joplinapp.org/plugins/) 32 | 33 | ## v1.3.5 (2023-12-26) 34 | 35 | - Fix: #64 With single JEX backups, some notebooks were backuped/exported twice 36 | 37 | ## v1.3.4 (2023-12-04) 38 | 39 | - Fix: #61 On Jex backup, do not overwrite already exported notebooks if the notebooks name differs only in special characters 40 | - Fix: #62 Better error message if the Joplin profile directory is set as backup path 41 | - Update en_US translation by @montoner0 42 | 43 | ## v1.3.3 (2023-07-08) 44 | 45 | - Add: Workaround for bug #132 with `"` (double quotes) in the password where zip files with such a password can no longer be opened 46 | 47 | ## v1.3.2 (2023-06-02) 48 | 49 | - Fix: #51 for translation zh_CN 50 | 51 | ## v1.3.1 (2023-05-19) 52 | 53 | - Add: #55 Simplified Chinese(zh_CN) translation, thanks to @wh201906 54 | 55 | ## v1.3.0 (2023-05-14) 56 | 57 | - Add: German translation 58 | 59 | ## v1.2.2 (2023-01-06) 60 | 61 | - Fix: `moveLogFile: ENOTDIR: not a directory` for certain backup settings 62 | - Add: DirectoryPath selector for backup path and tmp. export path selection on Joplin >= v2.10.4 63 | 64 | ## v1.2.1 (2022-12-10) 65 | 66 | - Fix: #47 Suppress repeating error message during automatic execution if the backup path is not accessible 67 | - Fix: #48 File already exists when a RAW or MD Frontmatter backup with no revisions is made 68 | 69 | ## v1.2.0 (2022-11-20) 70 | 71 | - Add: Option to select export format between JEX, MD with Frontmatter and RAW (Only supported with Joplin > 2.9.12) 72 | - Add: `Command on Backup finish` to execute a script/program when the backup is finished 73 | 74 | ## v1.1.1 (2022-07-27) 75 | 76 | - Fix: #45 Error message when deleting old ZIP backupsets 77 | - Disable Joplin DirectoryPath selector and use string input again because of several bugs [#6692](https://github.com/laurent22/joplin/issues/6692) and [#6693](https://github.com/laurent22/joplin/issues/6693) 78 | - Fix: #40 Delete existing temporary zip file to prevent adding files to an old aborted backup 79 | 80 | ## v1.1.0 (2022-07-11) 81 | 82 | - Improved: The default setting for `Single JEX` is now `true` (Create only one JEX backup file) to prevent the loss of internal notelinks during a restore. Joplin Discourse: [Lost all connections of my notes](https://discourse.joplinapp.org/t/lost-all-connections-of-my-notes/25464) 83 | - Improved: Create a sub folder `JoplinBackup` in the configured `Backup path` (Only for new installations). 84 | - Improved: Use new Joplin DirectoryPath selector for path selection. Not supported in Joplin >= v2.9.1, non-compatible Joplin versions still use a text input field. 85 | - Add: Backup all installed Joplin plugins (Only the jpl files, no plugin settings!) 86 | 87 | ## v1.0.5 (2021-12-05) 88 | 89 | - Fix: #28 No message was displayed that the backup is finished when starting manually 90 | - Improved: #25 Check `Backup set name` for invalid characters 91 | 92 | ## v1.0.4 [pre-release] (2021-08-28) 93 | 94 | - Improved: #21 Password check (empty passwords) 95 | 96 | ## v1.0.3 (2021-08-11) 97 | 98 | - Fix: #19 Backups failed from Joplin version v2.2.5 and higher, due to the removed template function 99 | 100 | ## v1.0.2 (2021-07-19) 101 | 102 | - Improved: Use of moments token 103 | - Fix: #16 Prevent multiple simultaneous backup runs 104 | - Add: #11 Make zip compression level selectable 105 | - Fix: Delete old backup set information, if the backup set no longer exists 106 | 107 | ## v1.0.1 (2021-07-03) 108 | 109 | Release for Joplin plugin manager 110 | 111 | ## v1.0.0 [pre-release] (2021-06-20) 112 | 113 | - Add: Option for encrypted backups 114 | 115 | > ❗️ Requires at least Joplin `2.1.3` ❗️ 116 | 117 | ## v0.9.0 [pre-release] (2021-06-19) 118 | 119 | - Add: Relative path could be used for `Backup Path` 120 | - Fix: An error with `Only on change`, which was not working properly 121 | - Add: Option to create zip archive 122 | - Add: Option to specify the `Backup set name` if multiple backups are to be keep. 123 | 124 | ## v0.5.3 (2021-04-03) 125 | 126 | - Add: Backup settings.json 127 | - Optimize: #7 Better error message when a backup is created twice in a row in the same minute 128 | 129 | ## v0.5.2 (2021-02-13) 130 | 131 | - Only internal changes 132 | 133 | ## v0.5.1 (2021-02-07) 134 | 135 | - Fix: Incomplete backup.log with only one backup set 136 | 137 | ## v0.5.0 (2021-02-07) 138 | 139 | - Add: Option for Backuplogfile 140 | - Add: Option to create a backup only if something has changed #3 141 | 142 | ## v0.4.1 (2021-01-23) 143 | 144 | - Optimize: Store profile data in `profile` folder 145 | 146 | ## v0.4.0 (2021-01-21) 147 | 148 | - Add `templates` to backup 149 | - Optimize: Delete old backups at the end of the backup job 150 | 151 | ## v0.3.1 (2021-01-21) 152 | 153 | - Fix #1: Unsupported characters in filename 154 | 155 | ## v0.3.0 (2021-01-17) 156 | 157 | - Fix: Backup not only the last 50 notebooks 158 | - Add: Backup userchrome.css and userstyle.css 159 | - Add: Option to create single file JEX for all notebooks 160 | 161 | ## v0.2.2 (2021-01-17) 162 | 163 | - Fix: Check if keymap-desktop.json exists 164 | 165 | ## v0.2.1 (2021-01-16) 166 | 167 | - remove seconds from folder 168 | 169 | ## v0.2.0 (2021-01-14) 170 | 171 | - Add: Automatic backups every X hours 172 | 173 | ## v0.1.0 (2021-01-14) 174 | 175 | - First version 176 | -------------------------------------------------------------------------------- /FULLRESTORE.md: -------------------------------------------------------------------------------- 1 | # Full Joplin restore 2 | 3 | A Joplin Export (JEX) file created by the Backup plugin can be used to perform a full restore of all notebooks, notes and attachments. This should be carefully undertaken as a mistake could cause the notes in the clients and on the sync server to be duplicated. To avoid duplication it also requires that any clients attached to the server are cleared of their note data and are then re-synced after the restore. For those with large note collections and numerous, large files attached to the notes, this could take a long time for each client. Essentially undertaking a full restore is a disaster recovery exercise. If you wish to just re-upload your notes to the server or re-download your notes from the server please please refer to the Joplin settings for Synchronisation under "Show Advanced Settings". 4 | 5 | > When Joplin imports a JEX backup file **it treats every notebook and note within that backup as a new item**. All the items in the backup will get new unique reference numbers (however links between notes will be maintained). If the client the backup is being restored to already contains all or some of the notebooks and notes in the backup they will be duplicated. 6 | > 7 | > A JEX backup file **does not** contain any note history. It contains the notebooks, notes and attachments as they were at the time the backup was made. 8 | 9 | The following options for a restore are described here: 10 | 11 | - [Full Restore - Without fully resetting the Joplin clients](#full-restore---without-fully-resetting-the-joplin-clients) 12 | - [Full Restore - Fully resetting the Joplin clients](#full-restore---fully-resetting-the-joplin-clients) 13 | 14 | ## Full Restore - Without fully resetting the Joplin clients 15 | 16 | With this method the existing link to the synchronisation cloud storage server will be preserved and used. If End to End Encryption (E2EE) has been enabled the settings will be preserved and used. Any customisation and installed plugins will be retained. 17 | 18 | - Identify the Joplin desktop client that has the best Internet connection as this will be the primary machine for the restore. 19 | - Place a copy of the Joplin backup file on that machine. 20 | - Open the Joplin client on the primary machine and delete all notes and notebooks. 21 | - Empty the Joplin client trash folder (Joplin 3.0+). 22 | - Sync the Joplin client so that all the notes are deleted on the cloud storage server. 23 | - When the above sync is complete, sync any other clients so that all the notebooks and notes they contain are deleted. 24 | - In case the cloud storage server has limits on how much data can be transferred at once it is suggested that all clients other than the one on the primary machine are shut down. 25 | - When all clients are empty move to the primary machine and import the backup (`File > Import > JEX - Joplin Export File`). 26 | - Sync the client to the cloud storage server. If you have a large collection of notes and attachments this may tale some time. 27 | - Once complete, start each client in turn and sync. The restored notes will be downloaded. If you have a large collection of notes and attachments this may tale some time. 28 | - If you are certain that the cloud provider will not limit connections or download speed if a large volume of transfers are made, the other clients could be synced at the same time. 29 | 30 | ## Full Restore - Fully resetting the Joplin clients 31 | 32 | With this method the existing link to the synchronisation cloud storage server will be lost. If End to End Encryption (E2EE) has been enabled the settings will be lost. Any customisation and installed plugins will be lost but **copies are stored with the JEX file in the backup archive**. 33 | 34 | - Identify the Joplin desktop client that has the best Internet connection as this will be the primary machine for the restore. 35 | - Place a copy of the Joplin backup file on that machine. 36 | - If possible, open the Joplin client on the primary machine and delete all notes and notebooks. 37 | - Empty the Joplin client trash folder (Joplin 3.0+). 38 | - Sync the Joplin client so that all the notes are deleted on the cloud storage server, or access the cloud storage server directly and delete the files in the folder Joplin uses for syncing. 39 | - Completely shut down all clients (`File > Quit`), including the primary machine. 40 | - On Windows machines delete, rename or move the folder `C:\Users\username\.config\joplin-desktop` 41 | - On Linux and Mac machines delete, rename or move the folder `/home/username/.config/joplin-desktop` 42 | - On mobile devices use the mobile OS's App settings to delete the Joplin app data and cache. 43 | - Start the Joplin client on the primary machine. 44 | - Joplin will recreate the `joplin-desktop` folder and the client will be as if just installed with only the "Welcome" notes. 45 | - There will be no sync settings or E2EE settings. 46 | - Connect the primary machine to the cloud storage server using the required credentials. 47 | - Sync to the server (this will be just the "Welcome" notes and so should be very quick). 48 | - If required set up E2EE and re-sync (this will be just the "Welcome" notes and so should be very quick). 49 | - For every other client, open Joplin, delete the "Welcome" notes and connect to the cloud storage server being used for sync. 50 | - This will download the encrypted notes created by the primary machine, so when prompted by Joplin enter the E2EE password that was previously set on the primary machine (this will be just the "Welcome" notes and so should be very quick). 51 | - Once it is confirmed that all clients are connected and that E2EE is working, fully shut down all clients except on the primary machine. 52 | - Open the Joplin client on the primary machine and delete the "Welcome" notes / notebook. 53 | - Empty the Joplin client trash folder (Joplin 3.0+). 54 | - Import the backup (`File > Import > JEX - Joplin Export File`). 55 | - Sync the client to the cloud storage server. If you have a large collection of notes and attachments this may tale some time. 56 | - Once complete, start each client in turn and sync. The restored notes will be downloaded. If you have a large collection of notes and attachments this may tale some time. 57 | - If you are certain that the cloud provider will not limit connections or download speed if a large volume of transfers are made, the other clients could be synced at the same time. 58 | -------------------------------------------------------------------------------- /GENERATOR_DOC.md: -------------------------------------------------------------------------------- 1 | # Plugin development 2 | 3 | This documentation describes how to create a plugin, and how to work with the plugin builder framework and API. 4 | 5 | ## Installation 6 | 7 | First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 8 | 9 | ```bash 10 | npm install -g yo@4.3.1 11 | npm install -g generator-joplin 12 | ``` 13 | 14 | Then generate your new project: 15 | 16 | ```bash 17 | yo --node-package-manager npm joplin 18 | ``` 19 | 20 | ## Structure 21 | 22 | The main two files you will want to look at are: 23 | 24 | - `/src/index.ts`, which contains the entry point for the plugin source code. 25 | - `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc. 26 | 27 | The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts. 28 | 29 | ## Building the plugin 30 | 31 | The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin. 32 | 33 | To build the plugin, simply run `npm run dist`. 34 | 35 | The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript. 36 | 37 | ## Updating the manifest version number 38 | 39 | You can run `npm run updateVersion` to bump the patch part of the version number, so for example 1.0.3 will become 1.0.4. This script will update both the package.json and manifest.json version numbers so as to keep them in sync. 40 | 41 | ## Publishing the plugin 42 | 43 | To publish the plugin, add it to npmjs.com by running `npm publish`. Later on, a script will pick up your plugin and add it automatically to the Joplin plugin repository as long as the package satisfies these conditions: 44 | 45 | - In `package.json`, the name starts with "joplin-plugin-". For example, "joplin-plugin-toc". 46 | - In `package.json`, the keywords include "joplin-plugin". 47 | - In the `publish/` directory, there should be a .jpl and .json file (which are built by `npm run dist`) 48 | 49 | In general all this is done automatically by the plugin generator, which will set the name and keywords of package.json, and will put the right files in the "publish" directory. But if something doesn't work and your plugin doesn't appear in the repository, double-check the above conditions. 50 | 51 | ## Updating the plugin framework 52 | 53 | To update the plugin framework, run `npm run update`. 54 | 55 | In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched. 56 | 57 | The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file. 58 | 59 | ## External script files 60 | 61 | By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases: 62 | 63 | - The script is a TypeScript file - in which case it has to be compiled to JavaScript. 64 | 65 | - The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file. 66 | 67 | To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file. 68 | 69 | ## More information 70 | 71 | - [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html) 72 | - [Joplin Data API](https://joplinapp.org/help/api/references/rest_api) 73 | - [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/) 74 | - Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq) 75 | 76 | ## License 77 | 78 | MIT © Laurent Cozic 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gruber Alexander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Joplin Plugin: Backup 2 | 3 | A plugin to extend Joplin with a manual and automatic backup function. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | - [Installation](#installation) 15 | - [Replace Joplin built-in plugin via GUI](#replace-joplin-built-in-plugin-via-gui) 16 | - [Replace Joplin built-in plugin via file system](#replace-joplin-built-in-plugin-via-file-system) 17 | - [Usage](#usage) 18 | - [Options](#options) 19 | - [Keyboard Shortcuts](#keyboard-shortcuts) 20 | - [What is backed up](#what-is-backed-up) 21 | - [Restore](#restore) 22 | - [Settings](#settings) 23 | - [Notes](#notes) 24 | - [Restore a Single note](#restore-a-single-note) 25 | - [Full Joplin restore](#full-joplin-restore) 26 | - [FAQ](#faq) 27 | - [Internal Joplin links betwen notes are lost](#internal-joplin-links-betwen-notes-are-lost) 28 | - [Combine multiple JEX Files to one](#combine-multiple-jex-files-to-one) 29 | - [Open a JEX Backup file](#open-a-jex-backup-file) 30 | - [Are Note History Revisions backed up?](#are-note-history-revisions-backed-up) 31 | - [Are all Joplin profiles backed up?](#are-all-joplin-profiles-backed-up) 32 | - [The Joplin build-in version of the plugin cannot be updated](#the-joplin-build-in-version-of-the-plugin-cannot-be-updated) 33 | - [Can I use a Backup to speed up first Joplin sync?](#can-i-use-a-backup-to-speed-up-first-joplin-sync) 34 | - [Changelog](#changelog) 35 | 36 | 37 | 38 | 39 | ## Installation 40 | 41 | The plugin is installed as built-in plugin in Joplin version `2.14.6` and newer. 42 | The built-in plugin cannot be updated via GUI, to update to a other version replace the built-in version. 43 | 44 | ### Replace Joplin built-in plugin via GUI 45 | 46 | - Download the latest released JPL package (`io.github.jackgruber.backup.jpl`) from [here](https://github.com/JackGruber/joplin-plugin-backup/releases/latest) 47 | - Go to `Tools > Options > Plugins` in Joplin 48 | - Click on the gear wheel and select `Install from file` 49 | - Select the downloaded JPL file 50 | - Restart Joplin 51 | 52 | ### Replace Joplin built-in plugin via file system 53 | 54 | - Download the latest released JPL package (`io.github.jackgruber.backup.jpl`) from [here](https://github.com/JackGruber/joplin-plugin-backup/releases/latest) 55 | - Close Joplin 56 | - Got to your Joplin profile folder and place the JPL file in the `plugins` folder 57 | - Start Joplin 58 | 59 | ## Usage 60 | 61 | First configure the Plugin under `Tools > Options > Backup`! 62 | The plugin must be configured separately for each Joplin profile. 63 | 64 | Backups can be created manually with the command `Tools > Create backup` or are created automatically based on the configured interval. 65 | The backup started manually by `Create backup` respects all the settings except for the `Backups interval in hours`. 66 | 67 | ## Options 68 | 69 | Go to `Tools > Options > Backup` 70 | 71 | ## Keyboard Shortcuts 72 | 73 | Under `Options > Keyboard Shortcuts` you can assign a keyboard shortcut for the following commands: 74 | 75 | - `Create backup` 76 | 77 | ## What is backed up 78 | 79 | - Notebooks as JEX export (Empty notebooks are not backed up) 80 | - The `settings.json` (Joplin settings) 81 | - The `keymap-desktop.json` (Keyboard shortcuts) 82 | - The `userchrome.css` (Your Joplin customization) 83 | - The `userstyle.css` (Your Joplin customization) 84 | - The `templates` folder (Note templates) 85 | - The `plugin` folder (All installed plugins, no plugin settings!) 86 | 87 | ## Restore 88 | 89 | ### Settings 90 | 91 | To restore the Settings, copy the desired files from `\Profile` to the Joplin directory `.config\joplin-desktop`. 92 | The exact path can be found in Joplin under `Tools > Options > General`: 93 | 94 | 95 | 96 | ### Notes 97 | 98 | The notes are imported via `File > Import > JEX - Joplin Export File`. 99 | 100 | > Individual notes cannot be restored from the JEX file! 101 | 102 | The notes are imported additionally, no check for duplicates is performed. 103 | If the notebook in which the note was located already exists in your Joplin, then a "(1)" will be appended to the folder name. 104 | 105 | ### Restore a Single note 106 | 107 | 1. Create a new profile in Joplin via `File > Switch profile > Create new Profile` 108 | 2. Joplin switches automatically to the newly created profile 109 | 3. Import the Backup via `File > Import > JEX - Joplin Export File` 110 | 4. Search for the desired note 111 | 5. In the note overview, click on the note on the right and select `Export > JEX - Joplin Export File` 112 | 6. Save the file on your computer 113 | 7. Switch back to your orginal Joplin profil via `File > Switch profile > Default` 114 | 8. Import the exported note via `File > Import > JEX - Joplin Export File` and select the file from step 6 115 | 116 | ### Full Joplin restore 117 | 118 | See the guide for a [Full Joplin restore](FULLRESTORE.md) 119 | 120 | ## FAQ 121 | 122 | ### Internal Joplin links betwen notes are lost 123 | 124 | If several JEX files are imported and the notes have links to each other, these links will be lost. 125 | Therefore it is recommended to create a Single JEX Backup! 126 | 127 | ### Combine multiple JEX Files to one 128 | 129 | By combining the JEX files into one, the Joplin internal links will work again after the import. 130 | 131 | 1. Open one of the JEX files in a ZIP program like 7-Zip 132 | 2. Open a second JEX and add all files to the first JEX 133 | 3. Repeat step 2 for all files 134 | 4. Import first JEX which now contains all notes 135 | 136 | ### Open a JEX Backup file 137 | 138 | A Joplin JEX Backup file is a tar archive which can be opened with any zip program that supports TAR archive. 139 | The file names in the archive correspond to the Joplin internal IDs. 140 | 141 | ### Are Note History (Revisions) backed up? 142 | 143 | A JEX backup file **does not** contain any note history (revisions). It contains the notebooks, notes and attachments as they were at the time the backup was made. 144 | 145 | ### Are all Joplin profiles backed up? 146 | 147 | No, the backup must be configured for each profile. 148 | Profiles that are not active are not backed up, even if a backup has been configured. 149 | 150 | ### The Joplin build-in version of the plugin cannot be updated 151 | 152 | Yes, the build-in version only gets updates with Joplin updates, but can be replaced as described in the [Installation](#installation) step. 153 | 154 | ### Can I use a Backup to speed up first Joplin sync? 155 | 156 | No, because all items in the backup will get new unique IDs are assigned in Joplin during the import (however links between notes will be maintained). 157 | If this device is then synchronized with a synchronization target in which other clients already synchronize with the same notes, all notes are then available multiple times on the devices. 158 | Therefore, the same note is then available with different IDs in Joplin. 159 | 160 | ## Changelog 161 | 162 | See [CHANGELOG.md](CHANGELOG.md) 163 | -------------------------------------------------------------------------------- /__test__/help.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { helper } from "../src/helper"; 3 | 4 | describe("Test helper", function () { 5 | it(`validFileName`, async () => { 6 | const testCases = [ 7 | { 8 | fileName: "some test file.txt", 9 | expected: true, 10 | }, 11 | { 12 | fileName: "some ^test file.txt", 13 | expected: true, 14 | }, 15 | { 16 | fileName: "some :test file.txt", 17 | expected: false, 18 | }, 19 | { 20 | fileName: "some \\test file.txt", 21 | expected: false, 22 | }, 23 | { 24 | fileName: "some |test file.txt", 25 | expected: false, 26 | }, 27 | { 28 | fileName: "some /test file.txt", 29 | expected: false, 30 | }, 31 | { 32 | fileName: "some *test file.txt", 33 | expected: false, 34 | }, 35 | { 36 | fileName: "some ?test file.txt", 37 | expected: false, 38 | }, 39 | { 40 | fileName: "some test file.txt", 45 | expected: false, 46 | }, 47 | { 48 | fileName: "com9.txt", 49 | expected: false, 50 | }, 51 | { 52 | fileName: "nul.txt", 53 | expected: false, 54 | }, 55 | { 56 | fileName: "prn.txt", 57 | expected: false, 58 | }, 59 | { 60 | fileName: "con.txt", 61 | expected: false, 62 | }, 63 | { 64 | fileName: "lpt5.txt", 65 | expected: false, 66 | }, 67 | ]; 68 | 69 | for (const testCase of testCases) { 70 | expect(await helper.validFileName(testCase.fileName)).toBe( 71 | testCase.expected 72 | ); 73 | } 74 | }); 75 | it(`Compare versions`, async () => { 76 | const testCases = [ 77 | { 78 | version1: "2.9.12", 79 | version2: "2.9.12", 80 | expected: 0, 81 | }, 82 | { 83 | version1: "2.9.12", 84 | version2: "2.9.13", 85 | expected: -1, 86 | }, 87 | { 88 | version1: "2.9.13", 89 | version2: "2.9.12", 90 | expected: 1, 91 | }, 92 | { 93 | version1: "2.10.6", 94 | version2: "2.9.12", 95 | expected: 1, 96 | }, 97 | { 98 | version1: "3.10.6", 99 | version2: "2.11.8", 100 | expected: 1, 101 | }, 102 | { 103 | version1: "2.10.6", 104 | version2: "3.11.8", 105 | expected: -1, 106 | }, 107 | { 108 | version1: "2", 109 | version2: "2.1", 110 | expected: -1, 111 | }, 112 | { 113 | version1: "2.1", 114 | version2: "2", 115 | expected: 1, 116 | }, 117 | { 118 | version1: "3", 119 | version2: "2", 120 | expected: 1, 121 | }, 122 | { 123 | version1: "2", 124 | version2: "2", 125 | expected: 0, 126 | }, 127 | { 128 | version1: "3.11.8", 129 | version2: "3.11.8-a", 130 | expected: 0, 131 | }, 132 | { 133 | version1: "2", 134 | version2: "", 135 | expected: -2, 136 | }, 137 | { 138 | version1: "3.a.8", 139 | version2: "3.11.8", 140 | expected: -1, 141 | }, 142 | ]; 143 | 144 | for (const testCase of testCases) { 145 | expect( 146 | await helper.versionCompare(testCase.version1, testCase.version2) 147 | ).toBe(testCase.expected); 148 | } 149 | }); 150 | 151 | test.each([ 152 | // Equality 153 | ["/tmp/this/is/a/test", "/tmp/this/is/a/test", true], 154 | ["/tmp/test", "/tmp/test///", true], 155 | 156 | // Subdirectories 157 | ["/tmp", "/tmp/test", true], 158 | ["/tmp/", "/tmp/test", true], 159 | ["/tmp/", "/tmp/..test", true], 160 | ["/tmp/test", "/tmp/", false], 161 | 162 | // Different directories 163 | ["/tmp/", "/tmp/../test", false], 164 | ["/tmp/te", "/tmp/test", false], 165 | ["a", "/a", false], 166 | ["/a/b", "/b/c", false], 167 | ])( 168 | "isSubdirectoryOrEqual with POSIX paths (is %s the parent of %s?)", 169 | (path1, path2, expected) => { 170 | expect(helper.isSubdirectoryOrEqual(path1, path2, path.posix)).toBe( 171 | expected 172 | ); 173 | } 174 | ); 175 | 176 | test.each([ 177 | ["C:\\Users\\User\\", "C:\\Users\\User\\", true], 178 | ["D:\\Users\\User\\", "C:\\Users\\User\\", false], 179 | ["C:\\Users\\Userr\\", "C:\\Users\\User\\", false], 180 | ["C:\\Users\\User\\", "C:\\Users\\User\\.config\\joplin-desktop", true], 181 | ])( 182 | "isSubdirectoryOrEqual with Windows paths (is %s the parent of %s?)", 183 | (path1, path2, expected) => { 184 | expect(helper.isSubdirectoryOrEqual(path1, path2, path.win32)).toBe( 185 | expected 186 | ); 187 | } 188 | ); 189 | }); 190 | -------------------------------------------------------------------------------- /__test__/pw.test.ts: -------------------------------------------------------------------------------- 1 | import { Backup } from "../src/Backup"; 2 | import joplin from "api"; 3 | import { when } from "jest-when"; 4 | import { I18n } from "i18n"; 5 | 6 | let backup = null; 7 | 8 | let spyOnLogVerbose = null; 9 | let spyOnLogInfo = null; 10 | let spyOnLogWarn = null; 11 | let spyOnLogError = null; 12 | let spyOnShowError = null; 13 | 14 | describe("Password", function () { 15 | beforeEach(async () => { 16 | backup = new Backup() as any; 17 | 18 | spyOnLogVerbose = jest 19 | .spyOn(backup.log, "verbose") 20 | .mockImplementation(() => {}); 21 | spyOnLogInfo = jest.spyOn(backup.log, "info").mockImplementation(() => {}); 22 | spyOnLogWarn = jest.spyOn(backup.log, "warn").mockImplementation(() => {}); 23 | spyOnLogError = jest 24 | .spyOn(backup.log, "error") 25 | .mockImplementation(() => {}); 26 | 27 | spyOnShowError = jest 28 | .spyOn(backup, "showError") 29 | .mockImplementation(() => {}); 30 | }); 31 | 32 | afterEach(async () => { 33 | spyOnLogVerbose.mockReset(); 34 | spyOnLogInfo.mockReset(); 35 | spyOnLogWarn.mockReset(); 36 | spyOnLogError.mockReset(); 37 | spyOnShowError.mockReset(); 38 | }); 39 | 40 | it(`Check`, async () => { 41 | const spyOnsSettingsValue = jest.spyOn(joplin.settings, "value"); 42 | const spyOnsSettingsSetValue = jest.spyOn(joplin.settings, "setValue"); 43 | 44 | const testCases = [ 45 | { 46 | usePassword: false, 47 | password: "", 48 | passwordRepeat: "", 49 | expected: 0, 50 | called: 2, 51 | }, 52 | { 53 | usePassword: false, 54 | password: "test", 55 | passwordRepeat: "test", 56 | expected: 0, 57 | called: 2, 58 | }, 59 | { 60 | usePassword: false, 61 | password: "testA", 62 | passwordRepeat: "testB", 63 | expected: 0, 64 | called: 2, 65 | }, 66 | { 67 | usePassword: true, 68 | password: "test", 69 | passwordRepeat: "test", 70 | expected: 1, 71 | called: 0, 72 | }, 73 | { 74 | usePassword: true, 75 | password: "testA", 76 | passwordRepeat: "testB", 77 | expected: -1, 78 | called: 2, 79 | }, 80 | { 81 | usePassword: true, 82 | password: " ", 83 | passwordRepeat: " ", 84 | expected: -1, 85 | called: 2, 86 | }, 87 | { 88 | usePassword: true, 89 | password: "", 90 | passwordRepeat: " ", 91 | expected: -1, 92 | called: 2, 93 | }, 94 | ]; 95 | 96 | for (const testCase of testCases) { 97 | /* prettier-ignore */ 98 | when(spyOnsSettingsValue) 99 | .mockImplementation(() => Promise.resolve("no mockImplementation")) 100 | .calledWith("usePassword").mockImplementation(() => Promise.resolve(testCase.usePassword)) 101 | .calledWith("password").mockImplementation(() => Promise.resolve(testCase.password)) 102 | .calledWith("passwordRepeat").mockImplementation(() => Promise.resolve(testCase.passwordRepeat)); 103 | expect(await backup.checkPassword()).toBe(testCase.expected); 104 | 105 | await backup.enablePassword(); 106 | 107 | if (testCase.expected == 1) { 108 | expect(backup.password).toBe(testCase.password); 109 | } 110 | expect(spyOnsSettingsSetValue).toBeCalledTimes(testCase.called); 111 | expect(backup.log.error).toHaveBeenCalledTimes(0); 112 | expect(backup.log.warn).toHaveBeenCalledTimes(0); 113 | spyOnsSettingsSetValue.mockReset(); 114 | } 115 | }); 116 | 117 | it(`Check node-7z bug`, async () => { 118 | const spyOnsSettingsValue = jest.spyOn(joplin.settings, "value"); 119 | const spyOnsSettingsSetValue = jest.spyOn(joplin.settings, "setValue"); 120 | jest.spyOn(backup, "getTranslation").mockImplementation(() => {}); 121 | const spyOnShowMsg = jest 122 | .spyOn(backup, "showMsg") 123 | .mockImplementation(() => {}); 124 | 125 | const testCases = [ 126 | { 127 | password: "1password", 128 | fail: false, 129 | }, 130 | { 131 | password: '2pass"word', 132 | fail: true, 133 | }, 134 | ]; 135 | 136 | for (const testCase of testCases) { 137 | when(spyOnsSettingsValue) 138 | .mockImplementation(() => Promise.resolve("no mockImplementation")) 139 | .calledWith("usePassword") 140 | .mockImplementation(() => Promise.resolve(true)) 141 | .calledWith("password") 142 | .mockImplementation(() => Promise.resolve(testCase.password)) 143 | .calledWith("passwordRepeat") 144 | .mockImplementation(() => Promise.resolve(testCase.password)); 145 | 146 | await backup.enablePassword(); 147 | 148 | if (testCase.fail == false) { 149 | expect(backup.password).toBe(testCase.password); 150 | expect(backup.log.error).toHaveBeenCalledTimes(0); 151 | expect(spyOnShowMsg).toHaveBeenCalledTimes(0); 152 | } else { 153 | expect(backup.password).toBe(null); 154 | expect(backup.log.error).toHaveBeenCalledTimes(1); 155 | expect(spyOnShowMsg).toHaveBeenCalledTimes(1); 156 | } 157 | 158 | expect(backup.log.warn).toHaveBeenCalledTimes(0); 159 | spyOnsSettingsSetValue.mockReset(); 160 | spyOnShowMsg.mockReset(); 161 | } 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /__test__/sevenZip.test.ts: -------------------------------------------------------------------------------- 1 | import { sevenZip } from "../src/sevenZip"; 2 | import * as path from "path"; 3 | import * as fs from "fs-extra"; 4 | 5 | const testBaseDir = path.join(__dirname, "ziptests"); 6 | 7 | describe("Test sevenZip", function () { 8 | beforeAll(async () => {}); 9 | 10 | beforeEach(async () => { 11 | fs.emptyDirSync(testBaseDir); 12 | }); 13 | 14 | afterAll(async () => { 15 | fs.removeSync(testBaseDir); 16 | }); 17 | 18 | it(`List`, async () => { 19 | const file1Name = "file1.txt"; 20 | const file2Name = "file2.txt"; 21 | const file3Name = "file3.txt"; 22 | const file1 = path.join(testBaseDir, file1Name); 23 | fs.emptyDirSync(path.join(testBaseDir, "sub")); 24 | const file2 = path.join(testBaseDir, "sub", file2Name); 25 | const file3 = path.join(testBaseDir, file3Name); 26 | const zip = path.join(testBaseDir, "file.7z"); 27 | fs.writeFileSync(file1, "file"); 28 | fs.writeFileSync(file2, "file"); 29 | fs.writeFileSync(file3, "file"); 30 | expect(fs.existsSync(file1)).toBe(true); 31 | expect(fs.existsSync(file2)).toBe(true); 32 | expect(fs.existsSync(file3)).toBe(true); 33 | expect(fs.existsSync(zip)).toBe(false); 34 | 35 | expect(await sevenZip.add(zip, path.join(testBaseDir, "*"), "secret")).toBe( 36 | true 37 | ); 38 | expect(fs.existsSync(zip)).toBe(true); 39 | 40 | expect(await sevenZip.passwordProtected(zip)).toBe(true); 41 | 42 | const fileList = await sevenZip.list(zip, "secret"); 43 | expect(fileList.length).toBe(4); 44 | }); 45 | 46 | it(`passwordProtected`, async () => { 47 | const fileName = "file.txt"; 48 | const file = path.join(testBaseDir, fileName); 49 | const zipNoPw = path.join(testBaseDir, "nowpw.7z"); 50 | const zippw = path.join(testBaseDir, "pw.7z"); 51 | 52 | fs.writeFileSync(file, "file"); 53 | expect(fs.existsSync(file)).toBe(true); 54 | 55 | expect(fs.existsSync(zipNoPw)).toBe(false); 56 | expect(await sevenZip.add(zipNoPw, file)).toBe(true); 57 | expect(fs.existsSync(zipNoPw)).toBe(true); 58 | expect(await sevenZip.passwordProtected(zipNoPw)).toBe(false); 59 | 60 | expect(fs.existsSync(zippw)).toBe(false); 61 | expect(await sevenZip.add(zippw, file, "secret")).toBe(true); 62 | expect(fs.existsSync(zippw)).toBe(true); 63 | expect(await sevenZip.passwordProtected(zippw)).toBe(true); 64 | }); 65 | 66 | describe("Add", function () { 67 | it(`File`, async () => { 68 | const fileName = "file.txt"; 69 | const file = path.join(testBaseDir, fileName); 70 | const zip = path.join(testBaseDir, "file.7z"); 71 | fs.writeFileSync(file, "file"); 72 | expect(fs.existsSync(file)).toBe(true); 73 | expect(fs.existsSync(zip)).toBe(false); 74 | 75 | const result = await sevenZip.add(zip, file); 76 | expect(result).toBe(true); 77 | expect(fs.existsSync(zip)).toBe(true); 78 | 79 | const sevenZipList = await sevenZip.list(zip); 80 | expect(sevenZipList.length).toBe(1); 81 | expect(sevenZipList[0].file).toBe(fileName); 82 | }); 83 | 84 | it(`File with password`, async () => { 85 | const fileName = "file.txt"; 86 | const file = path.join(testBaseDir, fileName); 87 | const zip = path.join(testBaseDir, "file.7z"); 88 | 89 | const passwords = ["scret", "bla!", 'VCe`,=/P<_+.7]~;Ys("']; 90 | 91 | for (const password of passwords) { 92 | fs.writeFileSync(file, "file"); 93 | expect(fs.existsSync(file)).toBe(true); 94 | expect(fs.existsSync(zip)).toBe(false); 95 | 96 | if (password.indexOf('"') >= 0) { 97 | let errorThrown = null; 98 | try { 99 | errorThrown = false; 100 | await sevenZip.add(zip, file, password, { method: ["x0"] }); 101 | } catch { 102 | errorThrown = true; 103 | } 104 | expect(errorThrown).toBe(true); 105 | } else { 106 | const result = await sevenZip.add(zip, file, password, { 107 | method: ["x0"], 108 | }); 109 | expect(result).toBe(true); 110 | expect(fs.existsSync(zip)).toBe(true); 111 | expect(await sevenZip.passwordProtected(zip)).toBe(true); 112 | const sevenZipList = await sevenZip.list(zip, password); 113 | 114 | expect(sevenZipList.length).toBe(1); 115 | expect(sevenZipList[0].file).toBe(fileName); 116 | } 117 | fs.removeSync(zip); 118 | } 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /__test__/sevenZipUpdateBinPath.test.ts: -------------------------------------------------------------------------------- 1 | import { sevenZip, pathTo7zip } from "../src/sevenZip"; 2 | import * as path from "path"; 3 | import joplin from "api"; 4 | 5 | it(`Set bin from joplin`, async () => { 6 | const pathBevor = pathTo7zip; 7 | const pathAdd = "addJoplinPath"; 8 | const pathAfter = path.join(pathAdd, "7zip-bin", pathTo7zip); 9 | 10 | jest.spyOn(joplin.plugins, "installationDir").mockImplementation(async () => { 11 | return pathAdd; 12 | }); 13 | 14 | await sevenZip.updateBinPath(); 15 | expect(pathTo7zip).toBe(pathAfter); 16 | }); 17 | -------------------------------------------------------------------------------- /api/Global.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import Joplin from './Joplin'; 3 | /** 4 | * @ignore 5 | */ 6 | /** 7 | * @ignore 8 | */ 9 | export default class Global { 10 | private joplin_; 11 | constructor(implementation: any, plugin: Plugin, store: any); 12 | get joplin(): Joplin; 13 | get process(): any; 14 | } 15 | -------------------------------------------------------------------------------- /api/Joplin.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import JoplinData from './JoplinData'; 3 | import JoplinPlugins from './JoplinPlugins'; 4 | import JoplinWorkspace from './JoplinWorkspace'; 5 | import JoplinFilters from './JoplinFilters'; 6 | import JoplinCommands from './JoplinCommands'; 7 | import JoplinViews from './JoplinViews'; 8 | import JoplinInterop from './JoplinInterop'; 9 | import JoplinSettings from './JoplinSettings'; 10 | import JoplinContentScripts from './JoplinContentScripts'; 11 | import JoplinClipboard from './JoplinClipboard'; 12 | import JoplinWindow from './JoplinWindow'; 13 | import BasePlatformImplementation from '../BasePlatformImplementation'; 14 | import JoplinImaging from './JoplinImaging'; 15 | /** 16 | * This is the main entry point to the Joplin API. You can access various services using the provided accessors. 17 | * 18 | * The API is now relatively stable and in general maintaining backward compatibility is a top priority, so you shouldn't except much breakages. 19 | * 20 | * If a breaking change ever becomes needed, best effort will be done to: 21 | * 22 | * - Deprecate features instead of removing them, so as to give you time to fix the issue; 23 | * - Document breaking changes in the changelog; 24 | * 25 | * So if you are developing a plugin, please keep an eye on the changelog as everything will be in there with information about how to update your code. 26 | */ 27 | export default class Joplin { 28 | private data_; 29 | private plugins_; 30 | private imaging_; 31 | private workspace_; 32 | private filters_; 33 | private commands_; 34 | private views_; 35 | private interop_; 36 | private settings_; 37 | private contentScripts_; 38 | private clipboard_; 39 | private window_; 40 | private implementation_; 41 | constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: any); 42 | get data(): JoplinData; 43 | get clipboard(): JoplinClipboard; 44 | get imaging(): JoplinImaging; 45 | get window(): JoplinWindow; 46 | get plugins(): JoplinPlugins; 47 | get workspace(): JoplinWorkspace; 48 | get contentScripts(): JoplinContentScripts; 49 | /** 50 | * @ignore 51 | * 52 | * Not sure if it's the best way to hook into the app 53 | * so for now disable filters. 54 | */ 55 | get filters(): JoplinFilters; 56 | get commands(): JoplinCommands; 57 | get views(): JoplinViews; 58 | get interop(): JoplinInterop; 59 | get settings(): JoplinSettings; 60 | /** 61 | * It is not possible to bundle native packages with a plugin, because they 62 | * need to work cross-platforms. Instead access to certain useful native 63 | * packages is provided using this function. 64 | * 65 | * Currently these packages are available: 66 | * 67 | * - [sqlite3](https://www.npmjs.com/package/sqlite3) 68 | * - [fs-extra](https://www.npmjs.com/package/fs-extra) 69 | * 70 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/nativeModule) 71 | */ 72 | require(_path: string): any; 73 | versionInfo(): Promise; 74 | } 75 | -------------------------------------------------------------------------------- /api/JoplinClipboard.d.ts: -------------------------------------------------------------------------------- 1 | export default class JoplinClipboard { 2 | private electronClipboard_; 3 | private electronNativeImage_; 4 | constructor(electronClipboard: any, electronNativeImage: any); 5 | readText(): Promise; 6 | writeText(text: string): Promise; 7 | readHtml(): Promise; 8 | writeHtml(html: string): Promise; 9 | /** 10 | * Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. 11 | */ 12 | readImage(): Promise; 13 | /** 14 | * Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format. 15 | */ 16 | writeImage(dataUrl: string): Promise; 17 | /** 18 | * Returns the list available formats (mime types). 19 | * 20 | * For example [ 'text/plain', 'text/html' ] 21 | */ 22 | availableFormats(): Promise; 23 | } 24 | -------------------------------------------------------------------------------- /api/JoplinCommands.d.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './types'; 2 | /** 3 | * This class allows executing or registering new Joplin commands. Commands 4 | * can be executed or associated with 5 | * {@link JoplinViewsToolbarButtons | toolbar buttons} or 6 | * {@link JoplinViewsMenuItems | menu items}. 7 | * 8 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 9 | * 10 | * ## Executing Joplin's internal commands 11 | * 12 | * It is also possible to execute internal Joplin's commands which, as of 13 | * now, are not well documented. You can find the list directly on GitHub 14 | * though at the following locations: 15 | * 16 | * * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands) 17 | * * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands) 18 | * * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts) 19 | * 20 | * To view what arguments are supported, you can open any of these files 21 | * and look at the `execute()` command. 22 | * 23 | * ## Executing editor commands 24 | * 25 | * There might be a situation where you want to invoke editor commands 26 | * without using a {@link JoplinContentScripts | contentScript}. For this 27 | * reason Joplin provides the built in `editor.execCommand` command. 28 | * 29 | * `editor.execCommand` should work with any core command in both the 30 | * [CodeMirror](https://codemirror.net/doc/manual.html#execCommand) and 31 | * [TinyMCE](https://www.tiny.cloud/docs/api/tinymce/tinymce.editorcommands/#execcommand) editors, 32 | * as well as most functions calls directly on a CodeMirror editor object (extensions). 33 | * 34 | * * [CodeMirror commands](https://codemirror.net/doc/manual.html#commands) 35 | * * [TinyMCE core editor commands](https://www.tiny.cloud/docs/advanced/editor-command-identifiers/#coreeditorcommands) 36 | * 37 | * `editor.execCommand` supports adding arguments for the commands. 38 | * 39 | * ```typescript 40 | * await joplin.commands.execute('editor.execCommand', { 41 | * name: 'madeUpCommand', // CodeMirror and TinyMCE 42 | * args: [], // CodeMirror and TinyMCE 43 | * ui: false, // TinyMCE only 44 | * value: '', // TinyMCE only 45 | * }); 46 | * ``` 47 | * 48 | * [View the example using the CodeMirror editor](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/codemirror_content_script/src/index.ts) 49 | * 50 | */ 51 | export default class JoplinCommands { 52 | /** 53 | * desktop Executes the given 54 | * command. 55 | * 56 | * The command can take any number of arguments, and the supported 57 | * arguments will vary based on the command. For custom commands, this 58 | * is the `args` passed to the `execute()` function. For built-in 59 | * commands, you can find the supported arguments by checking the links 60 | * above. 61 | * 62 | * ```typescript 63 | * // Create a new note in the current notebook: 64 | * await joplin.commands.execute('newNote'); 65 | * 66 | * // Create a new sub-notebook under the provided notebook 67 | * // Note: internally, notebooks are called "folders". 68 | * await joplin.commands.execute('newFolder', "SOME_FOLDER_ID"); 69 | * ``` 70 | */ 71 | execute(commandName: string, ...args: any[]): Promise; 72 | /** 73 | * desktop Registers a new command. 74 | * 75 | * ```typescript 76 | * // Register a new commmand called "testCommand1" 77 | * 78 | * await joplin.commands.register({ 79 | * name: 'testCommand1', 80 | * label: 'My Test Command 1', 81 | * iconName: 'fas fa-music', 82 | * execute: () => { 83 | * alert('Testing plugin command 1'); 84 | * }, 85 | * }); 86 | * ``` 87 | */ 88 | register(command: Command): Promise; 89 | } 90 | -------------------------------------------------------------------------------- /api/JoplinContentScripts.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ContentScriptType } from './types'; 3 | export default class JoplinContentScripts { 4 | private plugin; 5 | constructor(plugin: Plugin); 6 | /** 7 | * Registers a new content script. Unlike regular plugin code, which runs in 8 | * a separate process, content scripts run within the main process code and 9 | * thus allow improved performances and more customisations in specific 10 | * cases. It can be used for example to load a Markdown or editor plugin. 11 | * 12 | * Note that registering a content script in itself will do nothing - it 13 | * will only be loaded in specific cases by the relevant app modules (eg. 14 | * the Markdown renderer or the code editor). So it is not a way to inject 15 | * and run arbitrary code in the app, which for safety and performance 16 | * reasons is not supported. 17 | * 18 | * The plugin generator provides a way to build any content script you might 19 | * want to package as well as its dependencies. See the [Plugin Generator 20 | * doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md) 21 | * for more information. 22 | * 23 | * * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) 24 | * * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) 25 | * 26 | * See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) 27 | * 28 | * @param type Defines how the script will be used. See the type definition for more information about each supported type. 29 | * @param id A unique ID for the content script. 30 | * @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`. 31 | */ 32 | register(type: ContentScriptType, id: string, scriptPath: string): Promise; 33 | /** 34 | * Listens to a messages sent from the content script using postMessage(). 35 | * See {@link ContentScriptType} for more information as well as the 36 | * [postMessage 37 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) 38 | */ 39 | onMessage(contentScriptId: string, callback: any): Promise; 40 | } 41 | -------------------------------------------------------------------------------- /api/JoplinData.d.ts: -------------------------------------------------------------------------------- 1 | import { ModelType } from '../../../BaseModel'; 2 | import Plugin from '../Plugin'; 3 | import { Path } from './types'; 4 | /** 5 | * This module provides access to the Joplin data API: https://joplinapp.org/help/api/references/rest_api 6 | * This is the main way to retrieve data, such as notes, notebooks, tags, etc. 7 | * or to update them or delete them. 8 | * 9 | * This is also what you would use to search notes, via the `search` endpoint. 10 | * 11 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/simple) 12 | * 13 | * In general you would use the methods in this class as if you were using a REST API. There are four methods that map to GET, POST, PUT and DELETE calls. 14 | * And each method takes these parameters: 15 | * 16 | * * `path`: This is an array that represents the path to the resource in the form `["resouceName", "resourceId", "resourceLink"]` (eg. ["tags", ":id", "notes"]). The "resources" segment is the name of the resources you want to access (eg. "notes", "folders", etc.). If not followed by anything, it will refer to all the resources in that collection. The optional "resourceId" points to a particular resources within the collection. Finally, an optional "link" can be present, which links the resource to a collection of resources. This can be used in the API for example to retrieve all the notes associated with a tag. 17 | * * `query`: (Optional) The query parameters. In a URL, this is the part after the question mark "?". In this case, it should be an object with key/value pairs. 18 | * * `data`: (Optional) Applies to PUT and POST calls only. The request body contains the data you want to create or modify, for example the content of a note or folder. 19 | * * `files`: (Optional) Used to create new resources and associate them with files. 20 | * 21 | * Please refer to the [Joplin API documentation](https://joplinapp.org/help/api/references/rest_api) for complete details about each call. As the plugin runs within the Joplin application **you do not need an authorisation token** to use this API. 22 | * 23 | * For example: 24 | * 25 | * ```typescript 26 | * // Get a note ID, title and body 27 | * const noteId = 'some_note_id'; 28 | * const note = await joplin.data.get(['notes', noteId], { fields: ['id', 'title', 'body'] }); 29 | * 30 | * // Get all folders 31 | * const folders = await joplin.data.get(['folders']); 32 | * 33 | * // Set the note body 34 | * await joplin.data.put(['notes', noteId], null, { body: "New note body" }); 35 | * 36 | * // Create a new note under one of the folders 37 | * await joplin.data.post(['notes'], null, { body: "my new note", title: "some title", parent_id: folders[0].id }); 38 | * ``` 39 | */ 40 | export default class JoplinData { 41 | private api_; 42 | private pathSegmentRegex_; 43 | private plugin; 44 | constructor(plugin: Plugin); 45 | private serializeApiBody; 46 | private pathToString; 47 | get(path: Path, query?: any): Promise; 48 | post(path: Path, query?: any, body?: any, files?: any[]): Promise; 49 | put(path: Path, query?: any, body?: any, files?: any[]): Promise; 50 | delete(path: Path, query?: any): Promise; 51 | itemType(itemId: string): Promise; 52 | resourcePath(resourceId: string): Promise; 53 | /** 54 | * Gets an item user data. User data are key/value pairs. The `key` can be any 55 | * arbitrary string, while the `value` can be of any type supported by 56 | * [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description) 57 | * 58 | * User data is synchronised across devices, and each value wil be merged based on their timestamp: 59 | * 60 | * - If value is modified by client 1, then modified by client 2, it will take the value from client 2 61 | * - If value is modified by client 1, then deleted by client 2, the value will be deleted after merge 62 | * - If value is deleted by client 1, then updated by client 2, the value will be restored and set to the value from client 2 after merge 63 | */ 64 | userDataGet(itemType: ModelType, itemId: string, key: string): Promise; 65 | /** 66 | * Sets a note user data. See {@link JoplinData.userDataGet} for more details. 67 | */ 68 | userDataSet(itemType: ModelType, itemId: string, key: string, value: T): Promise; 69 | /** 70 | * Deletes a note user data. See {@link JoplinData.userDataGet} for more details. 71 | */ 72 | userDataDelete(itemType: ModelType, itemId: string, key: string): Promise; 73 | } 74 | -------------------------------------------------------------------------------- /api/JoplinFilters.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @ignore 3 | * 4 | * Not sure if it's the best way to hook into the app 5 | * so for now disable filters. 6 | */ 7 | export default class JoplinFilters { 8 | on(name: string, callback: Function): Promise; 9 | off(name: string, callback: Function): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /api/JoplinImaging.d.ts: -------------------------------------------------------------------------------- 1 | import { Rectangle } from './types'; 2 | export interface Implementation { 3 | nativeImage: any; 4 | } 5 | export interface CreateFromBufferOptions { 6 | width?: number; 7 | height?: number; 8 | scaleFactor?: number; 9 | } 10 | export interface ResizeOptions { 11 | width?: number; 12 | height?: number; 13 | quality?: 'good' | 'better' | 'best'; 14 | } 15 | export type Handle = string; 16 | /** 17 | * Provides imaging functions to resize or process images. You create an image 18 | * using one of the `createFrom` functions, then use the other functions to 19 | * process the image. 20 | * 21 | * Images are associated with a handle which is what will be available to the 22 | * plugin. Once you are done with an image, free it using the `free()` function. 23 | * 24 | * [View the 25 | * example](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/imaging/src/index.ts) 26 | * 27 | */ 28 | export default class JoplinImaging { 29 | private implementation_; 30 | private images_; 31 | constructor(implementation: Implementation); 32 | private createImageHandle; 33 | private imageByHandle; 34 | private cacheImage; 35 | createFromPath(filePath: string): Promise; 36 | createFromResource(resourceId: string): Promise; 37 | getSize(handle: Handle): Promise; 38 | resize(handle: Handle, options?: ResizeOptions): Promise; 39 | crop(handle: Handle, rectange: Rectangle): Promise; 40 | toPngFile(handle: Handle, filePath: string): Promise; 41 | /** 42 | * Quality is between 0 and 100 43 | */ 44 | toJpgFile(handle: Handle, filePath: string, quality?: number): Promise; 45 | private tempFilePath; 46 | /** 47 | * Creates a new Joplin resource from the image data. The image will be 48 | * first converted to a JPEG. 49 | */ 50 | toJpgResource(handle: Handle, resourceProps: any, quality?: number): Promise; 51 | /** 52 | * Creates a new Joplin resource from the image data. The image will be 53 | * first converted to a PNG. 54 | */ 55 | toPngResource(handle: Handle, resourceProps: any): Promise; 56 | /** 57 | * Image data is not automatically deleted by Joplin so make sure you call 58 | * this method on the handle once you are done. 59 | */ 60 | free(handle: Handle): Promise; 61 | } 62 | -------------------------------------------------------------------------------- /api/JoplinInterop.d.ts: -------------------------------------------------------------------------------- 1 | import { ExportModule, ImportModule } from './types'; 2 | /** 3 | * Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format. 4 | * 5 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) 6 | * 7 | * To implement an import or export module, you would simply define an object with various event handlers that are called 8 | * by the application during the import/export process. 9 | * 10 | * See the documentation of the [[ExportModule]] and [[ImportModule]] for more information. 11 | * 12 | * You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/help/api/references/rest_api 13 | */ 14 | export default class JoplinInterop { 15 | registerExportModule(module: ExportModule): Promise; 16 | registerImportModule(module: ImportModule): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /api/JoplinPlugins.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ContentScriptType, Script } from './types'; 3 | /** 4 | * This class provides access to plugin-related features. 5 | */ 6 | export default class JoplinPlugins { 7 | private plugin; 8 | constructor(plugin: Plugin); 9 | /** 10 | * Registers a new plugin. This is the entry point when creating a plugin. You should pass a simple object with an `onStart` method to it. 11 | * That `onStart` method will be executed as soon as the plugin is loaded. 12 | * 13 | * ```typescript 14 | * joplin.plugins.register({ 15 | * onStart: async function() { 16 | * // Run your plugin code here 17 | * } 18 | * }); 19 | * ``` 20 | */ 21 | register(script: Script): Promise; 22 | /** 23 | * @deprecated Use joplin.contentScripts.register() 24 | */ 25 | registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise; 26 | /** 27 | * Gets the plugin own data directory path. Use this to store any 28 | * plugin-related data. Unlike [[installationDir]], any data stored here 29 | * will be persisted. 30 | */ 31 | dataDir(): Promise; 32 | /** 33 | * Gets the plugin installation directory. This can be used to access any 34 | * asset that was packaged with the plugin. This directory should be 35 | * considered read-only because any data you store here might be deleted or 36 | * re-created at any time. To store new persistent data, use [[dataDir]]. 37 | */ 38 | installationDir(): Promise; 39 | /** 40 | * @deprecated Use joplin.require() 41 | */ 42 | require(_path: string): any; 43 | } 44 | -------------------------------------------------------------------------------- /api/JoplinSettings.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { SettingItem, SettingSection } from './types'; 3 | export interface ChangeEvent { 4 | /** 5 | * Setting keys that have been changed 6 | */ 7 | keys: string[]; 8 | } 9 | export type ChangeHandler = (event: ChangeEvent) => void; 10 | export declare const namespacedKey: (pluginId: string, key: string) => string; 11 | /** 12 | * This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user. 13 | * 14 | * Settings are essentially key/value pairs. 15 | * 16 | * Note: Currently this API does **not** provide access to Joplin's built-in settings. This is by design as plugins that modify user settings could give unexpected results 17 | * 18 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/settings) 19 | */ 20 | export default class JoplinSettings { 21 | private plugin_; 22 | constructor(plugin: Plugin); 23 | /** 24 | * Registers new settings. 25 | * Note that registering a setting item is dynamic and will be gone next time Joplin starts. 26 | * What it means is that you need to register the setting every time the plugin starts (for example in the onStart event). 27 | * The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some 28 | * reason the plugin fails to start at some point. 29 | */ 30 | registerSettings(settings: Record): Promise; 31 | /** 32 | * @deprecated Use joplin.settings.registerSettings() 33 | * 34 | * Registers a new setting. 35 | */ 36 | registerSetting(key: string, settingItem: SettingItem): Promise; 37 | /** 38 | * Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts. 39 | */ 40 | registerSection(name: string, section: SettingSection): Promise; 41 | /** 42 | * Gets a setting value (only applies to setting you registered from your plugin) 43 | */ 44 | value(key: string): Promise; 45 | /** 46 | * Sets a setting value (only applies to setting you registered from your plugin) 47 | */ 48 | setValue(key: string, value: any): Promise; 49 | /** 50 | * Gets a global setting value, including app-specific settings and those set by other plugins. 51 | * 52 | * The list of available settings is not documented yet, but can be found by looking at the source code: 53 | * 54 | * https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142 55 | */ 56 | globalValue(key: string): Promise; 57 | /** 58 | * Called when one or multiple settings of your plugin have been changed. 59 | * - For performance reasons, this event is triggered with a delay. 60 | * - You will only get events for your own plugin settings. 61 | */ 62 | onChange(handler: ChangeHandler): Promise; 63 | } 64 | -------------------------------------------------------------------------------- /api/JoplinViews.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import JoplinViewsDialogs from './JoplinViewsDialogs'; 3 | import JoplinViewsMenuItems from './JoplinViewsMenuItems'; 4 | import JoplinViewsMenus from './JoplinViewsMenus'; 5 | import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons'; 6 | import JoplinViewsPanels from './JoplinViewsPanels'; 7 | import JoplinViewsNoteList from './JoplinViewsNoteList'; 8 | /** 9 | * This namespace provides access to view-related services. 10 | * 11 | * All view services provide a `create()` method which you would use to create the view object, whether it's a dialog, a toolbar button or a menu item. 12 | * In some cases, the `create()` method will return a [[ViewHandle]], which you would use to act on the view, for example to set certain properties or call some methods. 13 | */ 14 | export default class JoplinViews { 15 | private store; 16 | private plugin; 17 | private panels_; 18 | private menuItems_; 19 | private menus_; 20 | private toolbarButtons_; 21 | private dialogs_; 22 | private noteList_; 23 | private implementation_; 24 | constructor(implementation: any, plugin: Plugin, store: any); 25 | get dialogs(): JoplinViewsDialogs; 26 | get panels(): JoplinViewsPanels; 27 | get menuItems(): JoplinViewsMenuItems; 28 | get menus(): JoplinViewsMenus; 29 | get toolbarButtons(): JoplinViewsToolbarButtons; 30 | get noteList(): JoplinViewsNoteList; 31 | } 32 | -------------------------------------------------------------------------------- /api/JoplinViewsDialogs.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ButtonSpec, ViewHandle, DialogResult } from './types'; 3 | /** 4 | * Allows creating and managing dialogs. A dialog is modal window that 5 | * contains a webview and a row of buttons. You can update the 6 | * webview using the `setHtml` method. Dialogs are hidden by default and 7 | * you need to call `open()` to open them. Once the user clicks on a 8 | * button, the `open` call will return an object indicating what button was 9 | * clicked on. 10 | * 11 | * ## Retrieving form values 12 | * 13 | * If your HTML content included one or more forms, a `formData` object 14 | * will also be included with the key/value for each form. 15 | * 16 | * ## Special button IDs 17 | * 18 | * The following buttons IDs have a special meaning: 19 | * 20 | * - `ok`, `yes`, `submit`, `confirm`: They are considered "submit" buttons 21 | * - `cancel`, `no`, `reject`: They are considered "dismiss" buttons 22 | * 23 | * This information is used by the application to determine what action 24 | * should be done when the user presses "Enter" or "Escape" within the 25 | * dialog. If they press "Enter", the first "submit" button will be 26 | * automatically clicked. If they press "Escape" the first "dismiss" button 27 | * will be automatically clicked. 28 | * 29 | * [View the demo 30 | * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog) 31 | */ 32 | export default class JoplinViewsDialogs { 33 | private store; 34 | private plugin; 35 | private implementation_; 36 | constructor(implementation: any, plugin: Plugin, store: any); 37 | private controller; 38 | /** 39 | * Creates a new dialog 40 | */ 41 | create(id: string): Promise; 42 | /** 43 | * Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel" 44 | */ 45 | showMessageBox(message: string): Promise; 46 | /** 47 | * Displays a dialog to select a file or a directory. Same options and 48 | * output as 49 | * https://www.electronjs.org/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options 50 | */ 51 | showOpenDialog(options: any): Promise; 52 | /** 53 | * Sets the dialog HTML content 54 | */ 55 | setHtml(handle: ViewHandle, html: string): Promise; 56 | /** 57 | * Adds and loads a new JS or CSS files into the dialog. 58 | */ 59 | addScript(handle: ViewHandle, scriptPath: string): Promise; 60 | /** 61 | * Sets the dialog buttons. 62 | */ 63 | setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise; 64 | /** 65 | * Opens the dialog 66 | */ 67 | open(handle: ViewHandle): Promise; 68 | /** 69 | * Toggle on whether to fit the dialog size to the content or not. 70 | * When set to false, the dialog is set to 90vw and 80vh 71 | * @default true 72 | */ 73 | setFitToContent(handle: ViewHandle, status: boolean): Promise; 74 | } 75 | -------------------------------------------------------------------------------- /api/JoplinViewsMenuItems.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateMenuItemOptions, MenuItemLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating and managing menu items. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 7 | */ 8 | export default class JoplinViewsMenuItems { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | /** 13 | * Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter. 14 | */ 15 | create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /api/JoplinViewsMenus.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuItem, MenuItemLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating menus. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu) 7 | */ 8 | export default class JoplinViewsMenus { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | private registerCommandAccelerators; 13 | /** 14 | * Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the 15 | * menu as a sub-menu of the application build-in menus. 16 | */ 17 | create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /api/JoplinViewsNoteList.d.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'redux'; 2 | import Plugin from '../Plugin'; 3 | import { ListRenderer } from './noteListType'; 4 | /** 5 | * This API allows you to customise how each note in the note list is rendered. 6 | * The renderer you implement follows a unidirectional data flow. 7 | * 8 | * The app provides the required dependencies whenever a note is updated - you 9 | * process these dependencies, and return some props, which are then passed to 10 | * your template and rendered. See [[[ListRenderer]]] for a detailed description 11 | * of each property of the renderer. 12 | * 13 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer) 14 | * 15 | * The default list renderer is implemented using the same API, so it worth checking it too: 16 | * 17 | * [Default list renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts) 18 | */ 19 | export default class JoplinViewsNoteList { 20 | private plugin_; 21 | private store_; 22 | constructor(plugin: Plugin, store: Store); 23 | registerRenderer(renderer: ListRenderer): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /api/JoplinViewsPanels.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | import { ViewHandle } from './types'; 3 | /** 4 | * Allows creating and managing view panels. View panels currently are 5 | * displayed at the right of the sidebar and allows displaying any HTML 6 | * content (within a webview) and update it in real-time. For example it 7 | * could be used to display a table of content for the active note, or 8 | * display various metadata or graph. 9 | * 10 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc) 11 | */ 12 | export default class JoplinViewsPanels { 13 | private store; 14 | private plugin; 15 | constructor(plugin: Plugin, store: any); 16 | private controller; 17 | /** 18 | * Creates a new panel 19 | */ 20 | create(id: string): Promise; 21 | /** 22 | * Sets the panel webview HTML 23 | */ 24 | setHtml(handle: ViewHandle, html: string): Promise; 25 | /** 26 | * Adds and loads a new JS or CSS files into the panel. 27 | */ 28 | addScript(handle: ViewHandle, scriptPath: string): Promise; 29 | /** 30 | * Called when a message is sent from the webview (using postMessage). 31 | * 32 | * To post a message from the webview to the plugin use: 33 | * 34 | * ```javascript 35 | * const response = await webviewApi.postMessage(message); 36 | * ``` 37 | * 38 | * - `message` can be any JavaScript object, string or number 39 | * - `response` is whatever was returned by the `onMessage` handler 40 | * 41 | * Using this mechanism, you can have two-way communication between the 42 | * plugin and webview. 43 | * 44 | * See the [postMessage 45 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. 46 | * 47 | */ 48 | onMessage(handle: ViewHandle, callback: Function): Promise; 49 | /** 50 | * Sends a message to the webview. 51 | * 52 | * The webview must have registered a message handler prior, otherwise the message is ignored. Use; 53 | * 54 | * ```javascript 55 | * webviewApi.onMessage((message) => { ... }); 56 | * ``` 57 | * 58 | * - `message` can be any JavaScript object, string or number 59 | * 60 | * The view API may have only one onMessage handler defined. 61 | * This method is fire and forget so no response is returned. 62 | * 63 | * It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api. 64 | */ 65 | postMessage(handle: ViewHandle, message: any): void; 66 | /** 67 | * Shows the panel 68 | */ 69 | show(handle: ViewHandle, show?: boolean): Promise; 70 | /** 71 | * Hides the panel 72 | */ 73 | hide(handle: ViewHandle): Promise; 74 | /** 75 | * Tells whether the panel is visible or not 76 | */ 77 | visible(handle: ViewHandle): Promise; 78 | } 79 | -------------------------------------------------------------------------------- /api/JoplinViewsToolbarButtons.d.ts: -------------------------------------------------------------------------------- 1 | import { ToolbarButtonLocation } from './types'; 2 | import Plugin from '../Plugin'; 3 | /** 4 | * Allows creating and managing toolbar buttons. 5 | * 6 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command) 7 | */ 8 | export default class JoplinViewsToolbarButtons { 9 | private store; 10 | private plugin; 11 | constructor(plugin: Plugin, store: any); 12 | /** 13 | * Creates a new toolbar button and associate it with the given command. 14 | */ 15 | create(id: string, commandName: string, location: ToolbarButtonLocation): Promise; 16 | } 17 | -------------------------------------------------------------------------------- /api/JoplinWindow.d.ts: -------------------------------------------------------------------------------- 1 | import Plugin from '../Plugin'; 2 | export interface Implementation { 3 | injectCustomStyles(elementId: string, cssFilePath: string): Promise; 4 | } 5 | export default class JoplinWindow { 6 | private plugin_; 7 | private store_; 8 | private implementation_; 9 | constructor(implementation: Implementation, plugin: Plugin, store: any); 10 | /** 11 | * Loads a chrome CSS file. It will apply to the window UI elements, except 12 | * for the note viewer. It is the same as the "Custom stylesheet for 13 | * Joplin-wide app styles" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) 14 | * for an example. 15 | */ 16 | loadChromeCssFile(filePath: string): Promise; 17 | /** 18 | * Loads a note CSS file. It will apply to the note viewer, as well as any 19 | * exported or printed note. It is the same as the "Custom stylesheet for 20 | * rendered Markdown" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css) 21 | * for an example. 22 | */ 23 | loadNoteCssFile(filePath: string): Promise; 24 | } 25 | -------------------------------------------------------------------------------- /api/JoplinWorkspace.d.ts: -------------------------------------------------------------------------------- 1 | import { FolderEntity } from '../../database/types'; 2 | import { Disposable, MenuItem } from './types'; 3 | export interface EditContextMenuFilterObject { 4 | items: MenuItem[]; 5 | } 6 | type FilterHandler = (object: T) => Promise; 7 | declare enum ItemChangeEventType { 8 | Create = 1, 9 | Update = 2, 10 | Delete = 3 11 | } 12 | interface ItemChangeEvent { 13 | id: string; 14 | event: ItemChangeEventType; 15 | } 16 | interface SyncStartEvent { 17 | withErrors: boolean; 18 | } 19 | interface ResourceChangeEvent { 20 | id: string; 21 | } 22 | type ItemChangeHandler = (event: ItemChangeEvent) => void; 23 | type SyncStartHandler = (event: SyncStartEvent) => void; 24 | type ResourceChangeHandler = (event: ResourceChangeEvent) => void; 25 | /** 26 | * The workspace service provides access to all the parts of Joplin that 27 | * are being worked on - i.e. the currently selected notes or notebooks as 28 | * well as various related events, such as when a new note is selected, or 29 | * when the note content changes. 30 | * 31 | * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins) 32 | */ 33 | export default class JoplinWorkspace { 34 | private store; 35 | constructor(store: any); 36 | /** 37 | * Called when a new note or notes are selected. 38 | */ 39 | onNoteSelectionChange(callback: Function): Promise; 40 | /** 41 | * Called when the content of a note changes. 42 | * @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes. 43 | */ 44 | onNoteContentChange(callback: Function): Promise; 45 | /** 46 | * Called when the content of the current note changes. 47 | */ 48 | onNoteChange(handler: ItemChangeHandler): Promise; 49 | /** 50 | * Called when a resource is changed. Currently this handled will not be 51 | * called when a resource is added or deleted. 52 | */ 53 | onResourceChange(handler: ResourceChangeHandler): Promise; 54 | /** 55 | * Called when an alarm associated with a to-do is triggered. 56 | */ 57 | onNoteAlarmTrigger(handler: Function): Promise; 58 | /** 59 | * Called when the synchronisation process is starting. 60 | */ 61 | onSyncStart(handler: SyncStartHandler): Promise; 62 | /** 63 | * Called when the synchronisation process has finished. 64 | */ 65 | onSyncComplete(callback: Function): Promise; 66 | /** 67 | * Called just before the editor context menu is about to open. Allows 68 | * adding items to it. 69 | */ 70 | filterEditorContextMenu(handler: FilterHandler): void; 71 | /** 72 | * Gets the currently selected note 73 | */ 74 | selectedNote(): Promise; 75 | /** 76 | * Gets the currently selected folder. In some cases, for example during 77 | * search or when viewing a tag, no folder is actually selected in the user 78 | * interface. In that case, that function would return the last selected 79 | * folder. 80 | */ 81 | selectedFolder(): Promise; 82 | /** 83 | * Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes. 84 | */ 85 | selectedNoteIds(): Promise; 86 | } 87 | export {}; 88 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import type Joplin from './Joplin'; 2 | 3 | declare const joplin: Joplin; 4 | 5 | export default joplin; 6 | -------------------------------------------------------------------------------- /api/noteListType.d.ts: -------------------------------------------------------------------------------- 1 | import { Size } from './types'; 2 | type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; 3 | export declare enum ItemFlow { 4 | TopToBottom = "topToBottom", 5 | LeftToRight = "leftToRight" 6 | } 7 | export type RenderNoteView = Record; 8 | export interface OnChangeEvent { 9 | elementId: string; 10 | value: any; 11 | noteId: string; 12 | } 13 | export type OnRenderNoteHandler = (props: any) => Promise; 14 | export type OnChangeHandler = (event: OnChangeEvent) => Promise; 15 | /** 16 | * Most of these are the built-in note properties, such as `note.title`, 17 | * `note.todo_completed`, etc. 18 | * 19 | * Additionally, the `item.*` properties are specific to the rendered item. The 20 | * most important being `item.selected`, which you can use to display the 21 | * selected note in a different way. 22 | * 23 | * Finally some special properties are provided to make it easier to render 24 | * notes. In particular, if possible prefer `note.titleHtml` to `note.title` 25 | * since some important processing has already been done on the string, such as 26 | * handling the search highlighter and escaping. Since it's HTML and already 27 | * escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax, 28 | * which disables escaping). 29 | * 30 | * `notes.tag` gives you the list of tags associated with the note. 31 | * 32 | * `note.isWatched` tells you if the note is currently opened in an external 33 | * editor. In which case you would generally display some indicator. 34 | */ 35 | export type ListRendererDepependency = ListRendererDatabaseDependency | 'item.size.width' | 'item.size.height' | 'item.selected' | 'note.titleHtml' | 'note.isWatched' | 'note.tags'; 36 | export interface ListRenderer { 37 | /** 38 | * It must be unique to your plugin. 39 | */ 40 | id: string; 41 | /** 42 | * Can be top to bottom or left to right. Left to right gives you more 43 | * option to set the size of the items since you set both its width and 44 | * height. 45 | */ 46 | flow: ItemFlow; 47 | /** 48 | * The size of each item must be specified in advance for performance 49 | * reasons, and cannot be changed afterwards. If the item flow is top to 50 | * bottom, you only need to specificy the item height (the width will be 51 | * ignored). 52 | */ 53 | itemSize: Size; 54 | /** 55 | * The CSS is relative to the list item container. What will appear in the 56 | * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use 57 | * child combinator with guarantee it will only apply to your own items. In 58 | * this example, the styling will apply to `.note-list-item > .content`: 59 | * 60 | * ```css 61 | * > .content { 62 | * padding: 10px; 63 | * } 64 | * ``` 65 | * 66 | * In order to get syntax highlighting working here, it's recommended 67 | * installing an editor extension such as [es6-string-html VSCode 68 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 69 | */ 70 | itemCss?: string; 71 | /** 72 | * List the dependencies that your plugin needs to render the note list 73 | * items. Only these will be passed to your `onRenderNote` handler. Ensure 74 | * that you do not add more than what you need since there is a performance 75 | * penalty for each property. 76 | */ 77 | dependencies: ListRendererDepependency[]; 78 | /** 79 | * This is the HTML template that will be used to render the note list item. 80 | * This is a [Mustache template](https://github.com/janl/mustache.js) and it 81 | * will receive the variable you return from `onRenderNote` as tags. For 82 | * example, if you return a property named `formattedDate` from 83 | * `onRenderNote`, you can insert it in the template using `Created date: 84 | * {{formattedDate}}`. 85 | * 86 | * In order to get syntax highlighting working here, it's recommended 87 | * installing an editor extension such as [es6-string-html VSCode 88 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 89 | */ 90 | itemTemplate: string; 91 | /** 92 | * This user-facing text is used for example in the View menu, so that your 93 | * renderer can be selected. 94 | */ 95 | label: () => Promise; 96 | /** 97 | * This is where most of the real-time processing will happen. When a note 98 | * is rendered for the first time and every time it changes, this handler 99 | * receives the properties specified in the `dependencies` property. You can 100 | * then process them, load any additional data you need, and once done you 101 | * need to return the properties that are needed in the `itemTemplate` HTML. 102 | * Again, to use the formatted date example, you could have such a renderer: 103 | * 104 | * ```typescript 105 | * dependencies: [ 106 | * 'note.title', 107 | * 'note.created_time', 108 | * ], 109 | * 110 | * itemTemplate: // html 111 | * ` 112 | *
113 | * Title: {{note.title}}
114 | * Date: {{formattedDate}} 115 | *
116 | * `, 117 | * 118 | * onRenderNote: async (props: any) => { 119 | * const formattedDate = dayjs(props.note.created_time).format(); 120 | * return { 121 | * // Also return the props, so that note.title is available from the 122 | * // template 123 | * ...props, 124 | * formattedDate, 125 | * } 126 | * }, 127 | * ``` 128 | */ 129 | onRenderNote: OnRenderNoteHandler; 130 | /** 131 | * This handler allows adding some interacivity to the note renderer - 132 | * whenever an input element within the item is changed (for example, when a 133 | * checkbox is clicked, or a text input is changed), this `onChange` handler 134 | * is going to be called. 135 | * 136 | * You can inspect `event.elementId` to know which element had some changes, 137 | * and `event.value` to know the new value. `event.noteId` also tells you 138 | * what note is affected, so that you can potentially apply changes to it. 139 | * 140 | * You specify the element ID, by setting a `data-id` attribute on the 141 | * input. 142 | * 143 | * For example, if you have such a template: 144 | * 145 | * ```html 146 | *
147 | * 148 | *
149 | * ``` 150 | * 151 | * The event handler will receive an event with `elementId` set to 152 | * `noteTitleInput`. 153 | */ 154 | onChange?: OnChangeHandler; 155 | } 156 | export {}; 157 | -------------------------------------------------------------------------------- /api/noteListType.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable multiline-comment-style */ 2 | 3 | import { Size } from './types'; 4 | 5 | // AUTO-GENERATED by generate-database-type 6 | type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_'; 7 | // AUTO-GENERATED by generate-database-type 8 | 9 | export enum ItemFlow { 10 | TopToBottom = 'topToBottom', 11 | LeftToRight = 'leftToRight', 12 | } 13 | 14 | export type RenderNoteView = Record; 15 | 16 | export interface OnChangeEvent { 17 | elementId: string; 18 | value: any; 19 | noteId: string; 20 | } 21 | 22 | export type OnRenderNoteHandler = (props: any)=> Promise; 23 | export type OnChangeHandler = (event: OnChangeEvent)=> Promise; 24 | 25 | /** 26 | * Most of these are the built-in note properties, such as `note.title`, 27 | * `note.todo_completed`, etc. 28 | * 29 | * Additionally, the `item.*` properties are specific to the rendered item. The 30 | * most important being `item.selected`, which you can use to display the 31 | * selected note in a different way. 32 | * 33 | * Finally some special properties are provided to make it easier to render 34 | * notes. In particular, if possible prefer `note.titleHtml` to `note.title` 35 | * since some important processing has already been done on the string, such as 36 | * handling the search highlighter and escaping. Since it's HTML and already 37 | * escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax, 38 | * which disables escaping). 39 | * 40 | * `notes.tag` gives you the list of tags associated with the note. 41 | * 42 | * `note.isWatched` tells you if the note is currently opened in an external 43 | * editor. In which case you would generally display some indicator. 44 | */ 45 | export type ListRendererDepependency = 46 | ListRendererDatabaseDependency | 47 | 'item.size.width' | 48 | 'item.size.height' | 49 | 'item.selected' | 50 | 'note.titleHtml' | 51 | 'note.isWatched' | 52 | 'note.tags'; 53 | 54 | export interface ListRenderer { 55 | /** 56 | * It must be unique to your plugin. 57 | */ 58 | id: string; 59 | 60 | /** 61 | * Can be top to bottom or left to right. Left to right gives you more 62 | * option to set the size of the items since you set both its width and 63 | * height. 64 | */ 65 | flow: ItemFlow; 66 | 67 | /** 68 | * The size of each item must be specified in advance for performance 69 | * reasons, and cannot be changed afterwards. If the item flow is top to 70 | * bottom, you only need to specificy the item height (the width will be 71 | * ignored). 72 | */ 73 | itemSize: Size; 74 | 75 | /** 76 | * The CSS is relative to the list item container. What will appear in the 77 | * page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use 78 | * child combinator with guarantee it will only apply to your own items. In 79 | * this example, the styling will apply to `.note-list-item > .content`: 80 | * 81 | * ```css 82 | * > .content { 83 | * padding: 10px; 84 | * } 85 | * ``` 86 | * 87 | * In order to get syntax highlighting working here, it's recommended 88 | * installing an editor extension such as [es6-string-html VSCode 89 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 90 | */ 91 | itemCss?: string; 92 | 93 | /** 94 | * List the dependencies that your plugin needs to render the note list 95 | * items. Only these will be passed to your `onRenderNote` handler. Ensure 96 | * that you do not add more than what you need since there is a performance 97 | * penalty for each property. 98 | */ 99 | dependencies: ListRendererDepependency[]; 100 | 101 | /** 102 | * This is the HTML template that will be used to render the note list item. 103 | * This is a [Mustache template](https://github.com/janl/mustache.js) and it 104 | * will receive the variable you return from `onRenderNote` as tags. For 105 | * example, if you return a property named `formattedDate` from 106 | * `onRenderNote`, you can insert it in the template using `Created date: 107 | * {{formattedDate}}`. 108 | * 109 | * In order to get syntax highlighting working here, it's recommended 110 | * installing an editor extension such as [es6-string-html VSCode 111 | * extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) 112 | */ 113 | itemTemplate: string; 114 | 115 | /** 116 | * This user-facing text is used for example in the View menu, so that your 117 | * renderer can be selected. 118 | */ 119 | label: ()=> Promise; 120 | 121 | /** 122 | * This is where most of the real-time processing will happen. When a note 123 | * is rendered for the first time and every time it changes, this handler 124 | * receives the properties specified in the `dependencies` property. You can 125 | * then process them, load any additional data you need, and once done you 126 | * need to return the properties that are needed in the `itemTemplate` HTML. 127 | * Again, to use the formatted date example, you could have such a renderer: 128 | * 129 | * ```typescript 130 | * dependencies: [ 131 | * 'note.title', 132 | * 'note.created_time', 133 | * ], 134 | * 135 | * itemTemplate: // html 136 | * ` 137 | *
138 | * Title: {{note.title}}
139 | * Date: {{formattedDate}} 140 | *
141 | * `, 142 | * 143 | * onRenderNote: async (props: any) => { 144 | * const formattedDate = dayjs(props.note.created_time).format(); 145 | * return { 146 | * // Also return the props, so that note.title is available from the 147 | * // template 148 | * ...props, 149 | * formattedDate, 150 | * } 151 | * }, 152 | * ``` 153 | */ 154 | onRenderNote: OnRenderNoteHandler; 155 | 156 | /** 157 | * This handler allows adding some interacivity to the note renderer - 158 | * whenever an input element within the item is changed (for example, when a 159 | * checkbox is clicked, or a text input is changed), this `onChange` handler 160 | * is going to be called. 161 | * 162 | * You can inspect `event.elementId` to know which element had some changes, 163 | * and `event.value` to know the new value. `event.noteId` also tells you 164 | * what note is affected, so that you can potentially apply changes to it. 165 | * 166 | * You specify the element ID, by setting a `data-id` attribute on the 167 | * input. 168 | * 169 | * For example, if you have such a template: 170 | * 171 | * ```html 172 | *
173 | * 174 | *
175 | * ``` 176 | * 177 | * The event handler will receive an event with `elementId` set to 178 | * `noteTitleInput`. 179 | */ 180 | onChange?: OnChangeHandler; 181 | } 182 | -------------------------------------------------------------------------------- /api/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable multiline-comment-style */ 2 | 3 | // ================================================================= 4 | // Command API types 5 | // ================================================================= 6 | 7 | export interface Command { 8 | /** 9 | * Name of command - must be globally unique 10 | */ 11 | name: string; 12 | 13 | /** 14 | * Label to be displayed on menu items or keyboard shortcut editor for example. 15 | * If it is missing, it's assumed it's a private command, to be called programmatically only. 16 | * In that case the command will not appear in the shortcut editor or command panel, and logically 17 | * should not be used as a menu item. 18 | */ 19 | label?: string; 20 | 21 | /** 22 | * Icon to be used on toolbar buttons for example 23 | */ 24 | iconName?: string; 25 | 26 | /** 27 | * Code to be ran when the command is executed. It may return a result. 28 | */ 29 | execute(...args: any[]): Promise; 30 | 31 | /** 32 | * Defines whether the command should be enabled or disabled, which in turns 33 | * affects the enabled state of any associated button or menu item. 34 | * 35 | * The condition should be expressed as a "when-clause" (as in Visual Studio 36 | * Code). It's a simple boolean expression that evaluates to `true` or 37 | * `false`. It supports the following operators: 38 | * 39 | * Operator | Symbol | Example 40 | * -- | -- | -- 41 | * Equality | == | "editorType == markdown" 42 | * Inequality | != | "currentScreen != config" 43 | * Or | \|\| | "noteIsTodo \|\| noteTodoCompleted" 44 | * And | && | "oneNoteSelected && !inConflictFolder" 45 | * 46 | * Joplin, unlike VSCode, also supports parenthesis, which allows creating 47 | * more complex expressions such as `cond1 || (cond2 && cond3)`. Only one 48 | * level of parenthesis is possible (nested ones aren't supported). 49 | * 50 | * Currently the supported context variables aren't documented, but you can 51 | * find the list below: 52 | * 53 | * - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts) 54 | * - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts) 55 | * 56 | * Note: Commands are enabled by default unless you use this property. 57 | */ 58 | enabledCondition?: string; 59 | } 60 | 61 | // ================================================================= 62 | // Interop API types 63 | // ================================================================= 64 | 65 | export enum FileSystemItem { 66 | File = 'file', 67 | Directory = 'directory', 68 | } 69 | 70 | export enum ImportModuleOutputFormat { 71 | Markdown = 'md', 72 | Html = 'html', 73 | } 74 | 75 | /** 76 | * Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) for an example. 77 | * 78 | * In general, all the event handlers you'll need to implement take a `context` object as a first argument. This object will contain the export or import path as well as various optional properties, such as which notes or notebooks need to be exported. 79 | * 80 | * To get a better sense of what it will contain it can be useful to print it using `console.info(context)`. 81 | */ 82 | export interface ExportModule { 83 | /** 84 | * The format to be exported, eg "enex", "jex", "json", etc. 85 | */ 86 | format: string; 87 | 88 | /** 89 | * The description that will appear in the UI, for example in the menu item. 90 | */ 91 | description: string; 92 | 93 | /** 94 | * Whether the module will export a single file or multiple files in a directory. It affects the open dialog that will be presented to the user when using your exporter. 95 | */ 96 | target: FileSystemItem; 97 | 98 | /** 99 | * Only applies to single file exporters or importers 100 | * It tells whether the format can package multiple notes into one file. 101 | * For example JEX or ENEX can, but HTML cannot. 102 | */ 103 | isNoteArchive: boolean; 104 | 105 | /** 106 | * The extensions of the files exported by your module. For example, it is `["htm", "html"]` for the HTML module, and just `["jex"]` for the JEX module. 107 | */ 108 | fileExtensions?: string[]; 109 | 110 | /** 111 | * Called when the export process starts. 112 | */ 113 | onInit(context: ExportContext): Promise; 114 | 115 | /** 116 | * Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc. 117 | */ 118 | onProcessItem(context: ExportContext, itemType: number, item: any): Promise; 119 | 120 | /** 121 | * Called when a resource file needs to be exported. 122 | */ 123 | onProcessResource(context: ExportContext, resource: any, filePath: string): Promise; 124 | 125 | /** 126 | * Called when the export process is done. 127 | */ 128 | onClose(context: ExportContext): Promise; 129 | } 130 | 131 | export interface ImportModule { 132 | /** 133 | * The format to be exported, eg "enex", "jex", "json", etc. 134 | */ 135 | format: string; 136 | 137 | /** 138 | * The description that will appear in the UI, for example in the menu item. 139 | */ 140 | description: string; 141 | 142 | /** 143 | * Only applies to single file exporters or importers 144 | * It tells whether the format can package multiple notes into one file. 145 | * For example JEX or ENEX can, but HTML cannot. 146 | */ 147 | isNoteArchive: boolean; 148 | 149 | /** 150 | * The type of sources that are supported by the module. Tells whether the module can import files or directories or both. 151 | */ 152 | sources: FileSystemItem[]; 153 | 154 | /** 155 | * Tells the file extensions of the exported files. 156 | */ 157 | fileExtensions?: string[]; 158 | 159 | /** 160 | * Tells the type of notes that will be generated, either HTML or Markdown (default). 161 | */ 162 | outputFormat?: ImportModuleOutputFormat; 163 | 164 | /** 165 | * Called when the import process starts. There is only one event handler within which you should import the complete data. 166 | */ 167 | onExec(context: ImportContext): Promise; 168 | } 169 | 170 | export interface ExportOptions { 171 | format?: string; 172 | path?: string; 173 | sourceFolderIds?: string[]; 174 | sourceNoteIds?: string[]; 175 | // modulePath?: string; 176 | target?: FileSystemItem; 177 | } 178 | 179 | export interface ExportContext { 180 | destPath: string; 181 | options: ExportOptions; 182 | 183 | /** 184 | * You can attach your own custom data using this propery - it will then be passed to each event handler, allowing you to keep state from one event to the next. 185 | */ 186 | userData?: any; 187 | } 188 | 189 | export interface ImportContext { 190 | sourcePath: string; 191 | options: any; 192 | warnings: string[]; 193 | } 194 | 195 | // ================================================================= 196 | // Misc types 197 | // ================================================================= 198 | 199 | export interface Script { 200 | onStart?(event: any): Promise; 201 | } 202 | 203 | export interface Disposable { 204 | // dispose():void; 205 | } 206 | 207 | export enum ModelType { 208 | Note = 1, 209 | Folder = 2, 210 | Setting = 3, 211 | Resource = 4, 212 | Tag = 5, 213 | NoteTag = 6, 214 | Search = 7, 215 | Alarm = 8, 216 | MasterKey = 9, 217 | ItemChange = 10, 218 | NoteResource = 11, 219 | ResourceLocalState = 12, 220 | Revision = 13, 221 | Migration = 14, 222 | SmartFilter = 15, 223 | Command = 16, 224 | } 225 | 226 | export interface VersionInfo { 227 | version: string; 228 | profileVersion: number; 229 | syncVersion: number; 230 | } 231 | 232 | // ================================================================= 233 | // Menu types 234 | // ================================================================= 235 | 236 | export interface CreateMenuItemOptions { 237 | accelerator: string; 238 | } 239 | 240 | export enum MenuItemLocation { 241 | File = 'file', 242 | Edit = 'edit', 243 | View = 'view', 244 | Note = 'note', 245 | Tools = 'tools', 246 | Help = 'help', 247 | 248 | /** 249 | * @deprecated Do not use - same as NoteListContextMenu 250 | */ 251 | Context = 'context', 252 | 253 | // If adding an item here, don't forget to update isContextMenuItemLocation() 254 | 255 | /** 256 | * When a command is called from the note list context menu, the 257 | * command will receive the following arguments: 258 | * 259 | * - `noteIds:string[]`: IDs of the notes that were right-clicked on. 260 | */ 261 | NoteListContextMenu = 'noteListContextMenu', 262 | 263 | EditorContextMenu = 'editorContextMenu', 264 | 265 | /** 266 | * When a command is called from a folder context menu, the 267 | * command will receive the following arguments: 268 | * 269 | * - `folderId:string`: ID of the folder that was right-clicked on 270 | */ 271 | FolderContextMenu = 'folderContextMenu', 272 | 273 | /** 274 | * When a command is called from a tag context menu, the 275 | * command will receive the following arguments: 276 | * 277 | * - `tagId:string`: ID of the tag that was right-clicked on 278 | */ 279 | TagContextMenu = 'tagContextMenu', 280 | } 281 | 282 | export function isContextMenuItemLocation(location: MenuItemLocation): boolean { 283 | return [ 284 | MenuItemLocation.Context, 285 | MenuItemLocation.NoteListContextMenu, 286 | MenuItemLocation.EditorContextMenu, 287 | MenuItemLocation.FolderContextMenu, 288 | MenuItemLocation.TagContextMenu, 289 | ].includes(location); 290 | } 291 | 292 | export interface MenuItem { 293 | /** 294 | * Command that should be associated with the menu item. All menu item should 295 | * have a command associated with them unless they are a sub-menu. 296 | */ 297 | commandName?: string; 298 | 299 | /** 300 | * Arguments that should be passed to the command. They will be as rest 301 | * parameters. 302 | */ 303 | commandArgs?: any[]; 304 | 305 | /** 306 | * Set to "separator" to create a divider line 307 | */ 308 | type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'); 309 | 310 | /** 311 | * Accelerator associated with the menu item 312 | */ 313 | accelerator?: string; 314 | 315 | /** 316 | * Menu items that should appear below this menu item. Allows creating a menu tree. 317 | */ 318 | submenu?: MenuItem[]; 319 | 320 | /** 321 | * Menu item label. If not specified, the command label will be used instead. 322 | */ 323 | label?: string; 324 | } 325 | 326 | // ================================================================= 327 | // View API types 328 | // ================================================================= 329 | 330 | export interface ButtonSpec { 331 | id: ButtonId; 332 | title?: string; 333 | onClick?(): void; 334 | } 335 | 336 | export type ButtonId = string; 337 | 338 | export enum ToolbarButtonLocation { 339 | /** 340 | * This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata. 341 | */ 342 | NoteToolbar = 'noteToolbar', 343 | 344 | /** 345 | * This toolbar is right above the text editor. It applies to the note body only. 346 | */ 347 | EditorToolbar = 'editorToolbar', 348 | } 349 | 350 | export type ViewHandle = string; 351 | 352 | export interface EditorCommand { 353 | name: string; 354 | value?: any; 355 | } 356 | 357 | export interface DialogResult { 358 | id: ButtonId; 359 | formData?: any; 360 | } 361 | 362 | export interface Size { 363 | width?: number; 364 | height?: number; 365 | } 366 | 367 | export interface Rectangle { 368 | x?: number; 369 | y?: number; 370 | width?: number; 371 | height?: number; 372 | } 373 | 374 | // ================================================================= 375 | // Settings types 376 | // ================================================================= 377 | 378 | export enum SettingItemType { 379 | Int = 1, 380 | String = 2, 381 | Bool = 3, 382 | Array = 4, 383 | Object = 5, 384 | Button = 6, 385 | } 386 | 387 | export enum SettingItemSubType { 388 | FilePathAndArgs = 'file_path_and_args', 389 | FilePath = 'file_path', // Not supported on mobile! 390 | DirectoryPath = 'directory_path', // Not supported on mobile! 391 | } 392 | 393 | export enum AppType { 394 | Desktop = 'desktop', 395 | Mobile = 'mobile', 396 | Cli = 'cli', 397 | } 398 | 399 | export enum SettingStorage { 400 | Database = 1, 401 | File = 2, 402 | } 403 | 404 | // Redefine a simplified interface to mask internal details 405 | // and to remove function calls as they would have to be async. 406 | export interface SettingItem { 407 | value: any; 408 | type: SettingItemType; 409 | 410 | /** 411 | * Currently only used to display a file or directory selector. Always set 412 | * `type` to `SettingItemType.String` when using this property. 413 | */ 414 | subType?: SettingItemSubType; 415 | 416 | label: string; 417 | description?: string; 418 | 419 | /** 420 | * A public setting will appear in the Configuration screen and will be 421 | * modifiable by the user. A private setting however will not appear there, 422 | * and can only be changed programmatically. You may use this to store some 423 | * values that you do not want to directly expose. 424 | */ 425 | public: boolean; 426 | 427 | /** 428 | * You would usually set this to a section you would have created 429 | * specifically for the plugin. 430 | */ 431 | section?: string; 432 | 433 | /** 434 | * To create a setting with multiple options, set this property to `true`. 435 | * That setting will render as a dropdown list in the configuration screen. 436 | */ 437 | isEnum?: boolean; 438 | 439 | /** 440 | * This property is required when `isEnum` is `true`. In which case, it 441 | * should contain a map of value => label. 442 | */ 443 | options?: Record; 444 | 445 | /** 446 | * Reserved property. Not used at the moment. 447 | */ 448 | appTypes?: AppType[]; 449 | 450 | /** 451 | * Set this to `true` to store secure data, such as passwords. Any such 452 | * setting will be stored in the system keychain if one is available. 453 | */ 454 | secure?: boolean; 455 | 456 | /** 457 | * An advanced setting will be moved under the "Advanced" button in the 458 | * config screen. 459 | */ 460 | advanced?: boolean; 461 | 462 | /** 463 | * Set the min, max and step values if you want to restrict an int setting 464 | * to a particular range. 465 | */ 466 | minimum?: number; 467 | maximum?: number; 468 | step?: number; 469 | 470 | /** 471 | * Either store the setting in the database or in settings.json. Defaults to database. 472 | */ 473 | storage?: SettingStorage; 474 | } 475 | 476 | export interface SettingSection { 477 | label: string; 478 | iconName?: string; 479 | description?: string; 480 | name?: string; 481 | } 482 | 483 | // ================================================================= 484 | // Data API types 485 | // ================================================================= 486 | 487 | /** 488 | * An array of at least one element and at most three elements. 489 | * 490 | * - **[0]**: Resource name (eg. "notes", "folders", "tags", etc.) 491 | * - **[1]**: (Optional) Resource ID. 492 | * - **[2]**: (Optional) Resource link. 493 | */ 494 | export type Path = string[]; 495 | 496 | // ================================================================= 497 | // Content Script types 498 | // ================================================================= 499 | 500 | export type PostMessageHandler = (message: any)=> Promise; 501 | 502 | /** 503 | * When a content script is initialised, it receives a `context` object. 504 | */ 505 | export interface ContentScriptContext { 506 | /** 507 | * The plugin ID that registered this content script 508 | */ 509 | pluginId: string; 510 | 511 | /** 512 | * The content script ID, which may be necessary to post messages 513 | */ 514 | contentScriptId: string; 515 | 516 | /** 517 | * Can be used by CodeMirror content scripts to post a message to the plugin 518 | */ 519 | postMessage: PostMessageHandler; 520 | } 521 | 522 | export interface ContentScriptModuleLoadedEvent { 523 | userData?: any; 524 | } 525 | 526 | export interface ContentScriptModule { 527 | onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void; 528 | plugin: ()=> any; 529 | assets?: ()=> void; 530 | } 531 | 532 | export interface MarkdownItContentScriptModule extends Omit { 533 | plugin: (markdownIt: any, options: any)=> any; 534 | } 535 | 536 | export enum ContentScriptType { 537 | /** 538 | * Registers a new Markdown-It plugin, which should follow the template 539 | * below. 540 | * 541 | * ```javascript 542 | * module.exports = { 543 | * default: function(context) { 544 | * return { 545 | * plugin: function(markdownIt, pluginOptions) { 546 | * // ... 547 | * }, 548 | * assets: { 549 | * // ... 550 | * }, 551 | * } 552 | * } 553 | * } 554 | * ``` 555 | * 556 | * See [the 557 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) 558 | * for a simple Markdown-it plugin example. 559 | * 560 | * ## Exported members 561 | * 562 | * - The `context` argument is currently unused but could be used later on 563 | * to provide access to your own plugin so that the content script and 564 | * plugin can communicate. 565 | * 566 | * - The **required** `plugin` key is the actual Markdown-It plugin - check 567 | * the [official doc](https://github.com/markdown-it/markdown-it) for more 568 | * information. 569 | * 570 | * - Using the **optional** `assets` key you may specify assets such as JS 571 | * or CSS that should be loaded in the rendered HTML document. Check for 572 | * example the Joplin [Mermaid 573 | * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) 574 | * to see how the data should be structured. 575 | * 576 | * ## Getting the settings from the renderer 577 | * 578 | * You can access your plugin settings from the renderer by calling 579 | * `pluginOptions.settingValue("your-setting-key')`. 580 | * 581 | * ## Posting messages from the content script to your plugin 582 | * 583 | * The application provides the following function to allow executing 584 | * commands from the rendered HTML code: 585 | * 586 | * ```javascript 587 | * const response = await webviewApi.postMessage(contentScriptId, message); 588 | * ``` 589 | * 590 | * - `contentScriptId` is the ID you've defined when you registered the 591 | * content script. You can retrieve it from the 592 | * {@link ContentScriptContext | context}. 593 | * - `message` can be any basic JavaScript type (number, string, plain 594 | * object), but it cannot be a function or class instance. 595 | * 596 | * When you post a message, the plugin can send back a `response` thus 597 | * allowing two-way communication: 598 | * 599 | * ```javascript 600 | * await joplin.contentScripts.onMessage(contentScriptId, (message) => { 601 | * // Process message 602 | * return response; // Can be any object, string or number 603 | * }); 604 | * ``` 605 | * 606 | * See {@link JoplinContentScripts.onMessage} for more details, as well as 607 | * the [postMessage 608 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). 609 | * 610 | * ## Registering an existing Markdown-it plugin 611 | * 612 | * To include a regular Markdown-It plugin, that doesn't make use of any 613 | * Joplin-specific features, you would simply create a file such as this: 614 | * 615 | * ```javascript 616 | * module.exports = { 617 | * default: function(context) { 618 | * return { 619 | * plugin: require('markdown-it-toc-done-right'); 620 | * } 621 | * } 622 | * } 623 | * ``` 624 | */ 625 | MarkdownItPlugin = 'markdownItPlugin', 626 | 627 | /** 628 | * Registers a new CodeMirror plugin, which should follow the template 629 | * below. 630 | * 631 | * ```javascript 632 | * module.exports = { 633 | * default: function(context) { 634 | * return { 635 | * plugin: function(CodeMirror) { 636 | * // ... 637 | * }, 638 | * codeMirrorResources: [], 639 | * codeMirrorOptions: { 640 | * // ... 641 | * }, 642 | * assets: { 643 | * // ... 644 | * }, 645 | * } 646 | * } 647 | * } 648 | * ``` 649 | * 650 | * - The `context` argument is currently unused but could be used later on 651 | * to provide access to your own plugin so that the content script and 652 | * plugin can communicate. 653 | * 654 | * - The `plugin` key is your CodeMirror plugin. This is where you can 655 | * register new commands with CodeMirror or interact with the CodeMirror 656 | * instance as needed. 657 | * 658 | * - The `codeMirrorResources` key is an array of CodeMirror resources that 659 | * will be loaded and attached to the CodeMirror module. These are made up 660 | * of addons, keymaps, and modes. For example, for a plugin that want's to 661 | * enable clojure highlighting in code blocks. `codeMirrorResources` would 662 | * be set to `['mode/clojure/clojure']`. 663 | * 664 | * - The `codeMirrorOptions` key contains all the 665 | * [CodeMirror](https://codemirror.net/doc/manual.html#config) options 666 | * that will be set or changed by this plugin. New options can alse be 667 | * declared via 668 | * [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption), 669 | * and then have their value set here. For example, a plugin that enables 670 | * line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`. 671 | * 672 | * - Using the **optional** `assets` key you may specify **only** CSS assets 673 | * that should be loaded in the rendered HTML document. Check for example 674 | * the Joplin [Mermaid 675 | * plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts) 676 | * to see how the data should be structured. 677 | * 678 | * One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys 679 | * must be provided for the plugin to be valid. Having multiple or all 680 | * provided is also okay. 681 | * 682 | * See also the [demo 683 | * plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script) 684 | * for an example of all these keys being used in one plugin. 685 | * 686 | * ## Posting messages from the content script to your plugin 687 | * 688 | * In order to post messages to the plugin, you can use the postMessage 689 | * function passed to the {@link ContentScriptContext | context}. 690 | * 691 | * ```javascript 692 | * const response = await context.postMessage('messageFromCodeMirrorContentScript'); 693 | * ``` 694 | * 695 | * When you post a message, the plugin can send back a `response` thus 696 | * allowing two-way communication: 697 | * 698 | * ```javascript 699 | * await joplin.contentScripts.onMessage(contentScriptId, (message) => { 700 | * // Process message 701 | * return response; // Can be any object, string or number 702 | * }); 703 | * ``` 704 | * 705 | * See {@link JoplinContentScripts.onMessage} for more details, as well as 706 | * the [postMessage 707 | * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages). 708 | * 709 | */ 710 | CodeMirrorPlugin = 'codeMirrorPlugin', 711 | } 712 | -------------------------------------------------------------------------------- /img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /img/icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/joplin-plugin-backup/76e9913e321132c6a7da6cb48b73db092754ef82/img/icon_256.png -------------------------------------------------------------------------------- /img/icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/joplin-plugin-backup/76e9913e321132c6a7da6cb48b73db092754ef82/img/icon_32.png -------------------------------------------------------------------------------- /img/joplin_path_in_gui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/joplin-plugin-backup/76e9913e321132c6a7da6cb48b73db092754ef82/img/joplin_path_in_gui.jpg -------------------------------------------------------------------------------- /img/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/joplin-plugin-backup/76e9913e321132c6a7da6cb48b73db092754ef82/img/main.png -------------------------------------------------------------------------------- /img/showcase1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackGruber/joplin-plugin-backup/76e9913e321132c6a7da6cb48b73db092754ef82/img/showcase1.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joplin-plugin-backup", 3 | "version": "1.4.4", 4 | "scripts": { 5 | "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive", 6 | "prepare": "npm run dist && husky install", 7 | "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force", 8 | "release": "npm test && node ./node_modules/joplinplugindevtools/dist/createRelease.js", 9 | "preRelease": "npm test && node ./node_modules/joplinplugindevtools/dist/createRelease.js --prerelease", 10 | "gitRelease": "node ./node_modules/joplinplugindevtools/dist/createRelease.js --upload", 11 | "gitPreRelease": "node ./node_modules/joplinplugindevtools/dist/createRelease.js --upload --prerelease", 12 | "test": "jest", 13 | "updateVersion": "webpack --env joplin-plugin-config=updateVersion" 14 | }, 15 | "license": "MIT", 16 | "keywords": [ 17 | "joplin-plugin" 18 | ], 19 | "devDependencies": { 20 | "@types/jest": "^26.0.23", 21 | "@types/node": "^18.7.13", 22 | "axios": "^0.21.1", 23 | "chalk": "^4.1.0", 24 | "copy-webpack-plugin": "^11.0.0", 25 | "dotenv": "^10.0.0", 26 | "fs-extra": "^10.1.0", 27 | "glob": "^8.0.3", 28 | "husky": "^6.0.0", 29 | "jest": "^27.0.4", 30 | "jest-when": "^3.3.1", 31 | "joplinplugindevtools": "^1.0.16", 32 | "lint-staged": "^11.0.0", 33 | "mime": "^2.5.2", 34 | "on-build-webpack": "^0.1.0", 35 | "prettier": "2.3.0", 36 | "tar": "^6.1.11", 37 | "ts-jest": "^27.0.2", 38 | "ts-loader": "^9.3.1", 39 | "typescript": "^4.8.2", 40 | "webpack": "^5.74.0", 41 | "webpack-cli": "^4.10.0", 42 | "yargs": "^16.2.0", 43 | "@joplin/lib": "~2.9" 44 | }, 45 | "browser": { 46 | "fs": false, 47 | "child_process": false 48 | }, 49 | "dependencies": { 50 | "@types/i18n": "^0.13.6", 51 | "7zip-bin": "^5.1.1", 52 | "electron-log": "^4.3.1", 53 | "i18n": "^0.15.1", 54 | "moment": "^2.29.1", 55 | "node-7z": "^2.1.2" 56 | }, 57 | "lint-staged": { 58 | "**/*": "prettier --write --ignore-unknown" 59 | }, 60 | "jest": { 61 | "transform": { 62 | ".(ts|tsx)": "ts-jest" 63 | }, 64 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", 65 | "moduleFileExtensions": [ 66 | "ts", 67 | "tsx", 68 | "js" 69 | ], 70 | "moduleNameMapper": { 71 | "^api$": "/node_modules/joplinplugindevtools/dist/apiMock.js", 72 | "^api/types$": "/api/types" 73 | } 74 | }, 75 | "files": [ 76 | "publish" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /plugin.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extraScripts": [] 3 | } 4 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import joplin from "api"; 2 | import * as path from "path"; 3 | 4 | export namespace helper { 5 | export async function validFileName(fileName: string) { 6 | var regChar = /[:*?"<>\/|\\]+/; // forbidden characters \ / : * ? " < > | 7 | var rexNames = /^(nul|prn|con|lpt[0-9]|com[0-9])(\.|$)/i; // forbidden file names 8 | 9 | if (regChar.test(fileName) === true || rexNames.test(fileName) === true) { 10 | return false; 11 | } else { 12 | return true; 13 | } 14 | } 15 | 16 | export async function joplinVersionInfo(): Promise { 17 | try { 18 | return await joplin.versionInfo(); 19 | } catch (error) { 20 | return null; 21 | } 22 | } 23 | 24 | // -2: Error 25 | // -1: Lower version 26 | // 0: Version equal 27 | // 1: Higer verison 28 | export async function versionCompare( 29 | version1: string, 30 | version2: string 31 | ): Promise { 32 | if (version1.trim() === "" || version2.trim() === "") { 33 | return -2; 34 | } 35 | 36 | const vArray1 = version1.split("."); 37 | const vArray2 = version2.split("."); 38 | let result = null; 39 | 40 | let maxIndex = -1; 41 | if (vArray1.length >= vArray2.length) { 42 | maxIndex = vArray1.length; 43 | } else { 44 | maxIndex = vArray2.length; 45 | } 46 | 47 | for (let index = 0; index < maxIndex; index++) { 48 | let check1 = 0; 49 | if (index < vArray1.length) { 50 | check1 = parseInt(vArray1[index]); 51 | } 52 | 53 | let check2 = 0; 54 | if (index < vArray2.length) { 55 | check2 = parseInt(vArray2[index]); 56 | } 57 | 58 | if (check1 > check2) { 59 | return 1; 60 | } else if (check1 === check2) { 61 | result = 0; 62 | } else { 63 | return -1; 64 | } 65 | } 66 | 67 | return result; 68 | } 69 | 70 | // Doesn't resolve simlinks 71 | // See https://stackoverflow.com/questions/44892672/how-to-check-if-two-paths-are-the-same-in-npm 72 | // for possible alternative implementations. 73 | export function isSubdirectoryOrEqual( 74 | parent: string, 75 | possibleChild: string, 76 | 77 | // Testing only 78 | pathModule: typeof path = path 79 | ) { 80 | // Appending path.sep to handle this case: 81 | // parent: /a/b/test 82 | // possibleChild: /a/b/test2 83 | // "/a/b/test2".startsWith("/a/b/test") -> true, but 84 | // "/a/b/test2/".startsWith("/a/b/test/") -> false 85 | // 86 | // Note that .resolve removes trailing slashes. 87 | // 88 | const normalizedParent = pathModule.resolve(parent) + pathModule.sep; 89 | const normalizedChild = pathModule.resolve(possibleChild) + pathModule.sep; 90 | 91 | return normalizedChild.startsWith(normalizedParent); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import joplin from "api"; 2 | import { Backup } from "./Backup"; 3 | 4 | const backup = new Backup(); 5 | 6 | joplin.plugins.register({ 7 | onStart: async function () { 8 | console.log("Start Backup Plugin"); 9 | await backup.init(); 10 | 11 | joplin.settings.onChange(async (event: any) => { 12 | if (event.keys.indexOf("backupInterval") !== -1) { 13 | console.log("Backup interval changed"); 14 | await backup.startTimer(); 15 | } 16 | }); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/locales/de_DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "backup": { 4 | "completed": "Backup wurde erstellt" 5 | }, 6 | "error": { 7 | "PluginUpgrade": "Upgrade Fehler %s: %s", 8 | "folderCreation": "Fehler beim Ordner erstellen: %s", 9 | "ConfigureBackupPath": "Bitte einen Backup Pfad in `Joplin Tools > Options > Backup` konfigurieren", 10 | "PasswordMissMatch": "Passwörter stimmen nicht überein!", 11 | "BackupPathDontExist": "Der Backup Pfad '%s' existiert nicht!", 12 | "BackupAlreadyRunning": "Es läuft bereits ein Backup!", 13 | "Backup": "Backup Fehler für %s: %s", 14 | "fileCopy": "Fehler beim kopieren von Datei/Ordner in %s: %s", 15 | "deleteFile": "Fehler beim löschen von Datei/Ordner in %s: %s", 16 | "backupPathContainsImportantDir": "Der Sicherungspfad ist oder enthält ein wichtiges Verzeichnis (%s), das durch ein Backup überschrieben werden könnte. Ohne die Aktivierung der Unterordner-Einstellung ist dies nicht erlaubt!", 17 | "BackupSetNotSupportedChars": "Der Name des Backup-Sets enthält nicht zulässige Zeichen ( %s )!", 18 | "passwordDoubleQuotes": "Das Passwort enthält \" (Doppelte Anführungszeichen), diese sind wegen eines Bugs nicht erlaubt. Der Passwortschutz für die Backups wurde deaktivert!" 19 | } 20 | }, 21 | "settings": { 22 | "section": { 23 | "label": "Backup" 24 | }, 25 | "path": { 26 | "label": "Sicherungs Pfad", 27 | "description": "Speicherort für die Backups. Dieser Pfad ist exklusiv für die Joplin Backups wenn die Einstellungen für 'Erstellen eines Unterordners' deaktiviert wird, es dürfen sich dann keine anderen Daten darin befinden!" 28 | }, 29 | "exportPath": { 30 | "label": "Temporärer Export Pfad", 31 | "description": "Temporärer Pfad für den Datenexport aus Joplin, bevor die Daten in den %s verschoben werden" 32 | }, 33 | "backupRetention": { 34 | "label": "Behalte x Sicherungen", 35 | "description": "Wie viele Sicherungen aufbewahrt werden sollen. Wenn mehr als eine Version konfiguriert ist, werden die Ordner im Sicherungspfad entsprechend der Einstellung 'Sicherungsset Namen' erstellt" 36 | }, 37 | "backupInterval": { 38 | "label": "Sicherungsinterval in Stunden", 39 | "description": "0 = Automatisches Sicherung ist deaktivert" 40 | }, 41 | "onlyOnChange": { 42 | "label": "Nur bei änderung", 43 | "description": "Erstellt eine Sicherung im angegebenen Sicherungsintervall nur dann, wenn es eine Änderung in den Notizen, Tags, Dateien oder Notizbücher gab" 44 | }, 45 | "usePassword": { 46 | "label": "Passwort geschütztes Sicherung", 47 | "description": "Die Sicherung wird mittels verschlüsseltem Archive geschützt" 48 | }, 49 | "password": { 50 | "label": "Passwort", 51 | "description": "Wenn ein Passwort eingegeben wurde, sind die Sicherungen mit einem Passwort geschützt" 52 | }, 53 | "passwordRepeat": { 54 | "label": "Passwort wiederholen", 55 | "description": "Wiederholen Sie das Passwort, um dieses zu bestätigen" 56 | }, 57 | "fileLogLevel": { 58 | "label": "Protokollstufe", 59 | "description": "Protokollstufe für die Backup Logdatei", 60 | "value": { 61 | "false": "Aus", 62 | "verbose": "Ausführlich", 63 | "info": "Info", 64 | "warn": "Warnung", 65 | "error": "Fehler" 66 | } 67 | }, 68 | "createSubfolder": { 69 | "label": "Erstellen eines Unterordners", 70 | "description": "Erstellt einen Unterordner im konfigurierten %s. Nur deaktivieren, wenn sich keine weiteren Daten im %s befinden!" 71 | }, 72 | "createSubfolderPerProfile": { 73 | "label": "Unterordner für Joplin profile", 74 | "description": "Erstellt einen Unterordner innerhalb des Sicherungsverzeichnisses für das aktuelle Profil. Dadurch können mehrere Profile derselben Joplin Installation dasselbe Sicherungsverzeichnis verwenden, ohne dass Sicherungen anderer Profile überschrieben werden. Alle Profile, die dasselbe Sicherungsverzeichnis verwenden, müssen diese Einstellung aktiviert haben" 75 | }, 76 | "zipArchive": { 77 | "label": "Erstelle ein Archive", 78 | "description": "Backup Daten in einem Archiv speichern, wenn ein Passwortschutz für die Sicherung eingestellt ist wird immer ein Archiv erstellt", 79 | "value": { 80 | "no": "Nein", 81 | "yes": "Ja", 82 | "yesone": "Ja, eine archive Datei" 83 | } 84 | }, 85 | "compressionLevel": { 86 | "label": "ZIP Komprimierungsgrad", 87 | "description": "Komprimierungsgrad für das Archiv", 88 | "value": { 89 | "copy": "Kopieren (keine Komprimierung)", 90 | "fastest": "Am schnellsten", 91 | "fast": "Schnell", 92 | "normal": "Normal", 93 | "maximum": "Maximal", 94 | "ultra": "Ultra" 95 | } 96 | }, 97 | "backupSetName": { 98 | "label": "Sicherungsset Namen", 99 | "description": "Name des Sicherungssatzes, wenn mehrere Sicherungen aufbewahrt werden sollen. Moment Token (https://momentjs.com/docs/#/displaying/format/) können mittels {TOKEN} verwendet werden" 100 | }, 101 | "backupPlugins": { 102 | "label": "Plugins sichern", 103 | "description": "Plugin jpl Dateien mit sichern (Es werden keine Plugin Einstellungen gesichert!)" 104 | }, 105 | "exportFormat": { 106 | "label": "Export Format", 107 | "description": "Joplin Datenexportformat während der Sicherung" 108 | }, 109 | "singleJex": { 110 | "label": "Eine JEX Datei", 111 | "description": "Erstellt nur eine JEX Datei (Empfohlen, um den Verlust interner Notizverknüpfungen oder der Ordnerstruktur bei einer Wiederherstellung zu vermeiden!)" 112 | }, 113 | "execFinishCmd": { 114 | "label": "Befehl nach der Sicherung", 115 | "description": "Befehl/Program nach der Sicherung ausführen" 116 | } 117 | }, 118 | "backupReadme": "# Joplin Sicherung\n\nDieser Ordner enthält eine oder mehrere Sicherungen aus der Joplin Note Anwendung.\n\nSiehe [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) für Informationen wie eine Sicherung wieder hergestellt werden kann.", 119 | "command": { 120 | "createBackup": "Backup erstellen" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/locales/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "backup": { 4 | "completed": "Backup completed" 5 | }, 6 | "error": { 7 | "PluginUpgrade": "Upgrade error %s: %s", 8 | "folderCreation": "Error on folder creation: %s", 9 | "ConfigureBackupPath": "Please configure backup path in Joplin Tools > Options > Backup", 10 | "PasswordMissMatch": "Passwords do not match!", 11 | "BackupPathDontExist": "The Backup path '%s' does not exist!", 12 | "BackupAlreadyRunning": "A backup is already running!", 13 | "Backup": "Backup error for %s: %s", 14 | "fileCopy": "Error on file/folder copy in %s: %s", 15 | "deleteFile": "Error on file/folder delete in %s: %s", 16 | "backupPathContainsImportantDir": "The backup path is or contains an important directory (%s) that could be overwritten by a backup. Without enabling the subfolder setting, this is not allowed!", 17 | "BackupSetNotSupportedChars": "Backup set name does contain not allowed characters ( %s )!", 18 | "passwordDoubleQuotes": "Password contains \" (double quotes), these are not allowed because of a bug. Password protection for the backup is deactivated!" 19 | } 20 | }, 21 | "settings": { 22 | "section": { 23 | "label": "Backup" 24 | }, 25 | "path": { 26 | "label": "Backup path", 27 | "description": "Storage location for the backups. This path is exclusive to Joplin backups when the 'Create Subfolder' setting is disabled: there should be no other data in it!" 28 | }, 29 | "exportPath": { 30 | "label": "Temporary export path", 31 | "description": "Temporary path for data export from Joplin, before the data is moved to the backup path" 32 | }, 33 | "backupRetention": { 34 | "label": "Keep x backups", 35 | "description": "How many backups should be kept. If more than one version configured, folders are created in the Backup Path according to 'Backup set name' setting" 36 | }, 37 | "backupInterval": { 38 | "label": "Backup interval in hours", 39 | "description": "0 = disable automatic backup" 40 | }, 41 | "onlyOnChange": { 42 | "label": "Only on change", 43 | "description": "Creates a backup at the specified backup interval only if there was a change to a `note`, `tag`, `resource` or `notebook`" 44 | }, 45 | "usePassword": { 46 | "label": "Password protected backups", 47 | "description": "Protect the backups via encrypted archive" 48 | }, 49 | "password": { 50 | "label": "Password", 51 | "description": "If a password has been entered, the backups are protected with a password" 52 | }, 53 | "passwordRepeat": { 54 | "label": "Password (Repeat)", 55 | "description": "Repeat password to validate" 56 | }, 57 | "fileLogLevel": { 58 | "label": "Log level", 59 | "description": "Log level for the backup log file", 60 | "value": { 61 | "false": "Off", 62 | "verbose": "Verbose", 63 | "info": "Info", 64 | "warn": "Warning", 65 | "error": "Error" 66 | } 67 | }, 68 | "createSubfolder": { 69 | "label": "Create Subfolder", 70 | "description": "Create a subfolder in the configured {{backupPath}}. Deactivate only if there is no other data in the {{backupPath}}!" 71 | }, 72 | "createSubfolderPerProfile": { 73 | "label": "Create subfolder for Joplin profile", 74 | "description": "Create a subfolder within the backup directory for the current profile. This allows multiple profiles from the same Joplin installation to use the same backup directory without overwriting backups made from other profiles. All profiles that use the same backup directory must have this setting enabled." 75 | }, 76 | "zipArchive": { 77 | "label": "Create archive", 78 | "description": "Save backup data in an archive. If 'Password protected backups' is set, an archive is always created.", 79 | "value": { 80 | "no": "No", 81 | "yes": "Yes", 82 | "yesone": "Yes, one archive" 83 | } 84 | }, 85 | "compressionLevel": { 86 | "label": "Compression level", 87 | "description": "Compression level for archive", 88 | "value": { 89 | "copy": "Copy (no compression)", 90 | "fastest": "Fastest", 91 | "fast": "Fast", 92 | "normal": "Normal", 93 | "maximum": "Maximum", 94 | "ultra": "Ultra" 95 | } 96 | }, 97 | "backupSetName": { 98 | "label": "Backup set name", 99 | "description": "Name of the backup set if multiple backups are to be kept. Moment Token (https://momentjs.com/docs/#/displaying/format/) can be used with {TOKEN}" 100 | }, 101 | "backupPlugins": { 102 | "label": "Backup plugins", 103 | "description": "Backup plugin jpl files (No plugin settings!)" 104 | }, 105 | "exportFormat": { 106 | "label": "Export format", 107 | "description": "Joplin data export format during the backup" 108 | }, 109 | "singleJex": { 110 | "label": "Single JEX", 111 | "description": "Create only one JEX file (Recommended to prevent the loss of internal note links or folder structure during a restore!)" 112 | }, 113 | "execFinishCmd": { 114 | "label": "Command on Backup finish", 115 | "description": "Execute command when backup is finished" 116 | } 117 | }, 118 | "backupReadme": "# Joplin Backup\n\nThis folder contains one or more backups of data from the Joplin note taking application.\n\nSee the [Backup documentation](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) for information about how to restore from this backup.", 119 | "command": { 120 | "createBackup": "Create backup" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/locales/ro_MD.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "backup": { 4 | "completed": "Copie de siguranță finalizată" 5 | }, 6 | "error": { 7 | "PluginUpgrade": "Eroare actualizare %s: %s", 8 | "folderCreation": "Eroare la crearea dosarului: %s", 9 | "ConfigureBackupPath": "Te rog configurează calea pentru copia de siguranță în Joplin din meniul Unelte > Opțiuni > Backup", 10 | "PasswordMissMatch": "Parolele nu se potrivesc!", 11 | "BackupPathDontExist": "Calea „%s” pentru copia de siguranță nu există!", 12 | "BackupAlreadyRunning": "O copiere de siguranță este deja în desfășurare!", 13 | "Backup": "Eroare copie de siguranță pentru %s: %s", 14 | "fileCopy": "Eroare la copierea fișierului/dosarului în %s: %s", 15 | "deleteFile": "Eroare la ștergerea fișierului/dosarului în %s: %s", 16 | "backupPathContainsImportantDir": "Calea pentru copia de siguranță este sau conține un dosar important (%s) care ar putea fi suprascris de o copie de siguranță. Fără a activa setarea pentru subdosare, acest lucru nu este permis!", 17 | "BackupSetNotSupportedChars": "Numele setului copiei de siguranță conține caractere nepermise ( %s )!", 18 | "passwordDoubleQuotes": "Parola conține \" (ghilimele). Acestea nu sînt permise din cauza unui defect. Protectția cu parolă a copiei de siguranță este dezactivată!" 19 | } 20 | }, 21 | "settings": { 22 | "path": { 23 | "label": "Locație copie de siguranță", 24 | "description": "Calea către locația pentru stocarea cópiilor de siguranță. Această cale este folosită exclusiv de cópiile Joplin atunci cînd setarea „Creează subdosar” este dezactivată: nu ar trebui să fie niciun fel de alte date în ea!" 25 | }, 26 | "exportPath": { 27 | "label": "Cale de export temporară", 28 | "description": "Cale temporară pentru exportul de date din Joplin, folosită înainte de mutarea datelor în calea copiei de siguranță." 29 | }, 30 | "backupRetention": { 31 | "label": "Păstrează n cópii de siguranță", 32 | "description": "Cîte cópii de siguranță ar trebui păstrate. Dacă este configurată mai mult de o versiune, se vor creea dosare în calea dată în concordanță cu setarea „Nume set copie de siguranță”." 33 | }, 34 | "backupInterval": { 35 | "label": "Interval de copiere în ore", 36 | "description": "0 = dezactivează copierea automată" 37 | }, 38 | "onlyOnChange": { 39 | "label": "Doar la schimbare", 40 | "description": "Creează o copie de siguranță la intervalul de timp specificat doar dacă s-a detectat o schimbare la o notiță, etichetă, resursă sau caiet." 41 | }, 42 | "usePassword": { 43 | "label": "Protejează cópiile de siguranță cu parolă", 44 | "description": "Realizează o arhivă criptată a cópiilor." 45 | }, 46 | "password": { 47 | "label": "Parolă", 48 | "description": "Dacă este întrodusă o parolă, cópiile vor fi protejate." 49 | }, 50 | "passwordRepeat": { 51 | "label": "Parolă (din nou)", 52 | "description": "Repetă parola pentru a valida." 53 | }, 54 | "fileLogLevel": { 55 | "label": "Nivel juranlizare", 56 | "description": "Nivelul de severitate pentru jurnalul copiei de siguranță.", 57 | "value": { 58 | "false": "fără", 59 | "verbose": "detaliat", 60 | "info": "informativ", 61 | "warn": "atenționări", 62 | "error": "erori" 63 | } 64 | }, 65 | "createSubfolder": { 66 | "label": "Creează subdosar", 67 | "description": "Creează un subdosar în calea copiei de siguranță date. Dezactivează doar dacă nu există alte date în calea copiei de siguranță!" 68 | }, 69 | "createSubfolderPerProfile": { 70 | "label": "Creează subdosar pentru profilul Joplin", 71 | "description": "Creează un subdosar în dosarul copiei de siguranță pentru profilul curent. Acest lucru permite ca mai multe profile din instalarea curentă a Joplin să folosească același dosar de cópii de siguranță fără să suprascrie cópiile create de alte profile. Toate profilele care folosesc același dosar de cópii trebuie să aibă această opțiune activată." 72 | }, 73 | "zipArchive": { 74 | "label": "Creează arhivă", 75 | "description": "Salvează datele copiate într-o arhivă. Dacă „Protejează cópiile de siguranță cu parolă” este activat, o arhivă este mereu creată.", 76 | "value": { 77 | "no": "nu", 78 | "yes": "da", 79 | "yesone": "da, o singură arhivă" 80 | } 81 | }, 82 | "compressionLevel": { 83 | "label": "Nivel compresie", 84 | "description": "Gradul de compresie al arhivei", 85 | "value": { 86 | "copy": "fără compresie", 87 | "fastest": "cea mai rapidă", 88 | "fast": "rapidă", 89 | "normal": "normală", 90 | "maximum": "maximă", 91 | "ultra": "ultra" 92 | } 93 | }, 94 | "backupSetName": { 95 | "label": "Nume set copie de siguranță", 96 | "description": "Numele unui set de cópii dacă mai multe cópii trebuie păstrate. Token-uri Moment (https://momentjs.com/docs/#/displaying/format/) pot fi utilizate cu {TOKEN}" 97 | }, 98 | "backupPlugins": { 99 | "label": "Copiază plugin-urile", 100 | "description": "Copiază și fișierele jpl ale plugin-urilor (fără setări!)" 101 | }, 102 | "exportFormat": { 103 | "label": "Format export", 104 | "description": "Formatul folosit la exportarea notițelor din Joplin" 105 | }, 106 | "singleJex": { 107 | "label": "Un singur fișier JEX", 108 | "description": "Creează doar un singur fișier JEX (Recomandat pentru a preveni pierderile de date legate de legăturile dintre note și structura ierarhică în timpul restaurării!)" 109 | }, 110 | "execFinishCmd": { 111 | "label": "Comandă la finalizarea copierii de siguranță", 112 | "description": "Execută o comandă la terminarea copierii" 113 | } 114 | }, 115 | "backupReadme": "# Copie de siguranță Joplin\n\nAcest dosar conține una sau mai multe cópii de siguranță ale datelor din aplicația de notițe Joplin.\n\nVezi [documentația](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) pentru informații legate de cum să restaurezi copia de siguranță.", 116 | "command": { 117 | "createBackup": "Creează o copie de siguranță" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/locales/ro_RO.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "backup": { 4 | "completed": "Copie de siguranță finalizată" 5 | }, 6 | "error": { 7 | "PluginUpgrade": "Eroare actualizare %s: %s", 8 | "folderCreation": "Eroare la crearea dosarului: %s", 9 | "ConfigureBackupPath": "Te rog configurează calea pentru copia de siguranță în Joplin din meniul Unelte > Opțiuni > Backup", 10 | "PasswordMissMatch": "Parolele nu se potrivesc!", 11 | "BackupPathDontExist": "Calea „%s” pentru copia de siguranță nu există!", 12 | "BackupAlreadyRunning": "O copiere de siguranță este deja în desfășurare!", 13 | "Backup": "Eroare copie de siguranță pentru %s: %s", 14 | "fileCopy": "Eroare la copierea fișierului/dosarului în %s: %s", 15 | "deleteFile": "Eroare la ștergerea fișierului/dosarului în %s: %s", 16 | "backupPathContainsImportantDir": "Calea pentru copia de siguranță este sau conține un dosar important (%s) care ar putea fi suprascris de o copie de siguranță. Fără a activa setarea pentru subdosare, acest lucru nu este permis!", 17 | "BackupSetNotSupportedChars": "Numele setului copiei de siguranță conține caractere nepermise ( %s )!", 18 | "passwordDoubleQuotes": "Parola conține \" (ghilimele). Acestea nu sunt permise din cauza unui defect. Protectția cu parolă a copiei de siguranță este dezactivată!" 19 | } 20 | }, 21 | "settings": { 22 | "path": { 23 | "label": "Locație copie de siguranță", 24 | "description": "Calea către locația pentru stocarea cópiilor de siguranță. Această cale este folosită exclusiv de cópiile Joplin atunci când setarea „Creează subdosar” este dezactivată: nu ar trebui să fie niciun fel de alte date în ea!" 25 | }, 26 | "exportPath": { 27 | "label": "Cale de export temporară", 28 | "description": "Cale temporară pentru exportul de date din Joplin, folosită înainte de mutarea datelor în calea copiei de siguranță." 29 | }, 30 | "backupRetention": { 31 | "label": "Păstrează n cópii de siguranță", 32 | "description": "Câte cópii de siguranță ar trebui păstrate. Dacă este configurată mai mult de o versiune, se vor creea dosare în calea dată în concordanță cu setarea „Nume set copie de siguranță”." 33 | }, 34 | "backupInterval": { 35 | "label": "Interval de copiere în ore", 36 | "description": "0 = dezactivează copierea automată" 37 | }, 38 | "onlyOnChange": { 39 | "label": "Doar la schimbare", 40 | "description": "Creează o copie de siguranță la intervalul de timp specificat doar dacă s-a detectat o schimbare la o notiță, etichetă, resursă sau caiet." 41 | }, 42 | "usePassword": { 43 | "label": "Protejează cópiile de siguranță cu parolă", 44 | "description": "Realizează o arhivă criptată a cópiilor." 45 | }, 46 | "password": { 47 | "label": "Parolă", 48 | "description": "Dacă este introdusă o parolă, cópiile vor fi protejate." 49 | }, 50 | "passwordRepeat": { 51 | "label": "Parolă (din nou)", 52 | "description": "Repetă parola pentru a valida." 53 | }, 54 | "fileLogLevel": { 55 | "label": "Nivel juranlizare", 56 | "description": "Nivelul de severitate pentru jurnalul copiei de siguranță.", 57 | "value": { 58 | "false": "fără", 59 | "verbose": "detaliat", 60 | "info": "informativ", 61 | "warn": "atenționări", 62 | "error": "erori" 63 | } 64 | }, 65 | "createSubfolder": { 66 | "label": "Creează subdosar", 67 | "description": "Creează un subdosar în calea copiei de siguranță date. Dezactivează doar dacă nu există alte date în calea copiei de siguranță!" 68 | }, 69 | "createSubfolderPerProfile": { 70 | "label": "Creează subdosar pentru profilul Joplin", 71 | "description": "Creează un subdosar în dosarul copiei de siguranță pentru profilul curent. Acest lucru permite ca mai multe profile din instalarea curentă a Joplin să folosească același dosar de cópii de siguranță fără să suprascrie cópiile create de alte profile. Toate profilele care folosesc același dosar de cópii trebuie să aibă această opțiune activată." 72 | }, 73 | "zipArchive": { 74 | "label": "Creează arhivă", 75 | "description": "Salvează datele copiate într-o arhivă. Dacă „Protejează cópiile de siguranță cu parolă” este activat, o arhivă este mereu creată.", 76 | "value": { 77 | "no": "nu", 78 | "yes": "da", 79 | "yesone": "da, o singură arhivă" 80 | } 81 | }, 82 | "compressionLevel": { 83 | "label": "Nivel compresie", 84 | "description": "Gradul de compresie al arhivei", 85 | "value": { 86 | "copy": "fără compresie", 87 | "fastest": "cea mai rapidă", 88 | "fast": "rapidă", 89 | "normal": "normală", 90 | "maximum": "maximă", 91 | "ultra": "ultra" 92 | } 93 | }, 94 | "backupSetName": { 95 | "label": "Nume set copie de siguranță", 96 | "description": "Numele unui set de cópii dacă mai multe cópii trebuie păstrate. Token-uri Moment (https://momentjs.com/docs/#/displaying/format/) pot fi utilizate cu {TOKEN}" 97 | }, 98 | "backupPlugins": { 99 | "label": "Copiază plugin-urile", 100 | "description": "Copiază și fișierele jpl ale plugin-urilor (fără setări!)" 101 | }, 102 | "exportFormat": { 103 | "label": "Format export", 104 | "description": "Formatul folosit la exportarea notițelor din Joplin" 105 | }, 106 | "singleJex": { 107 | "label": "Un singur fișier JEX", 108 | "description": "Creează doar un singur fișier JEX (Recomandat pentru a preveni pierderile de date legate de legăturile dintre note și structura ierarhică în timpul restaurării!)" 109 | }, 110 | "execFinishCmd": { 111 | "label": "Comandă la finalizarea copierii de siguranță", 112 | "description": "Execută o comandă la terminarea copierii" 113 | } 114 | }, 115 | "backupReadme": "# Copie de siguranță Joplin\n\nAcest dosar conține una sau mai multe cópii de siguranță ale datelor din aplicația de notițe Joplin.\n\nVezi [documentația](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) pentru informații legate de cum să restaurezi copia de siguranță.", 116 | "command": { 117 | "createBackup": "Creează o copie de siguranță" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/locales/sk_SK.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "backup": { 4 | "completed": "Zálohovanie dokončené" 5 | }, 6 | "error": { 7 | "PluginUpgrade": "Chyba aktualizácie %s: %s", 8 | "folderCreation": "Chyba pri vytváraní priečinka: %s", 9 | "ConfigureBackupPath": "Prosím, nastavte cestu zálohy Joplin v ponuke Nástroje > Možnosti > Zálohovanie", 10 | "PasswordMissMatch": "Heslá sa nezhodujú!", 11 | "BackupPathDontExist": "Cesta zálohy '%s' neexistuje!", 12 | "BackupAlreadyRunning": "Zálohovanie už prebieha!", 13 | "Backup": "Chyba zálohovania pre %s: %s", 14 | "fileCopy": "Chyba pri kopírovaní súboru/priečinka v %s: %s", 15 | "deleteFile": "Chyba pri mazaní súboru/priečinka v %s: %s", 16 | "backupPathContainsImportantDir": "Cesta zálohy je alebo obsahuje dôležitý priečinok (%s), ktorý by mohol byť prepísaný zálohou. Bez povolenia nastavenia podpriečinka toto nie je povolené!", 17 | "BackupSetNotSupportedChars": "Názov záložnej sady obsahuje nepovolené znaky ( %s )!", 18 | "passwordDoubleQuotes": "Heslo obsahuje \" (úvodzovky). Tieto nie sú povolené kvôli chybe. Ochrana heslom pre zálohu je deaktivovaná!" 19 | } 20 | }, 21 | "settings": { 22 | "path": { 23 | "label": "Cesta zálohovania", 24 | "description": "Umiestnenie úložiska pre zálohy. Táto cesta je výhradne pre zálohy Joplin, keď je nastavenie 'Vytvoriť podpriečinok' zakázané: nemali by v ňom byť žiadne iné údaje!" 25 | }, 26 | "exportPath": { 27 | "label": "Dočasná cesta exportu", 28 | "description": "Dočasná cesta pre export údajov z aplikácie Joplin pred presunom údajov do zálohovacej cesty" 29 | }, 30 | "backupRetention": { 31 | "label": "Ponechať x záloh", 32 | "description": "Koľko záloh by sa malo zachovať. Ak je nastavená viac ako jedna verzia, priečinky sa vytvárajú v ceste zálohovania podľa nastavenia 'Názov záložnej sady'" 33 | }, 34 | "backupInterval": { 35 | "label": "Interval zálohovania v hodinách", 36 | "description": "0 = vypnúť automatické zálohovanie" 37 | }, 38 | "onlyOnChange": { 39 | "label": "Iba pri zmene", 40 | "description": "Vytvorí zálohu v zadanom intervale zálohovania iba vtedy, ak došlo v `poznámke`, `štítku`, `zdroji` alebo `zápisníku` ku zmene" 41 | }, 42 | "usePassword": { 43 | "label": "Zálohy chránené heslom", 44 | "description": "Chrániť zálohy prostredníctvom šifrovaného archívu" 45 | }, 46 | "password": { 47 | "label": "Heslo", 48 | "description": "Ak bolo zadané heslo, zálohy sú chránené heslom" 49 | }, 50 | "passwordRepeat": { 51 | "label": "Heslo (opakovať)", 52 | "description": "Zopakovať heslo pre overenie" 53 | }, 54 | "fileLogLevel": { 55 | "label": "Úroveň záznamu", 56 | "description": "Úroveň záznamu pre súbor záznamu zálohy", 57 | "value": { 58 | "false": "Vypnuté", 59 | "verbose": "Podrobné", 60 | "info": "Informácie", 61 | "warn": "Upozornenie", 62 | "error": "Chyba" 63 | } 64 | }, 65 | "createSubfolder": { 66 | "label": "Vytvoriť podpriečinok", 67 | "description": "Vytvoriť podpriečinok v nastavenej {backupPath}. Deaktivujte iba vtedy, ak v {backupPath} nie sú žiadne iné údaje!" 68 | }, 69 | "createSubfolderPerProfile": { 70 | "label": "Vytvoriť podpriečinok pre profil Joplin", 71 | "description": "Vytvoriť podpriečinok v rámci priečinku zálohy pre aktuálny profil. To umožňuje viacerým profilom z rovnakej inštalácie Joplin používať rovnaký priečinok zálohy bez prepisovania záloh vytvorených z iných profilov. Všetky profily, ktoré používajú rovnaký priečinok zálohy, musia mať toto nastavenie povolené." 72 | }, 73 | "zipArchive": { 74 | "label": "Vytvoriť archív", 75 | "description": "Uložiť zálohované údaje do archívu. Ak sú nastavené 'Zálohy chránené heslom', archív sa vždy vytvorí.", 76 | "value": { 77 | "no": "Nie", 78 | "yes": "Áno", 79 | "yesone": "Áno, jeden archív" 80 | } 81 | }, 82 | "compressionLevel": { 83 | "label": "Úroveň kompresie", 84 | "description": "Úroveň kompresie pre archív", 85 | "value": { 86 | "copy": "Kopírovať (žiadna kompresia)", 87 | "fastest": "Najrýchlejšia", 88 | "fast": "Rýchla", 89 | "normal": "Normálna", 90 | "maximum": "Maximálna", 91 | "ultra": "Ultra" 92 | } 93 | }, 94 | "backupSetName": { 95 | "label": "Názov záložnej sady", 96 | "description": "Názov záložnej sady, ak sa má uchovávať viacero záloh. Moment Token (https://momentjs.com/docs/#/displaying/format/) je možné použiť s {TOKEN}" 97 | }, 98 | "backupPlugins": { 99 | "label": "Zálohovať doplnky", 100 | "description": "Zálohovať jpl súbory doplnku (Nie nastavenia doplnku!)" 101 | }, 102 | "exportFormat": { 103 | "label": "Formát exportu", 104 | "description": "Formát exportu údajov aplikácie Joplin počas zálohovania" 105 | }, 106 | "singleJex": { 107 | "label": "Jediný JEX", 108 | "description": "Vytvoriť iba jeden JEX súbor (Odporúča sa, aby sa predišlo strate interných odkazov na poznámky alebo štruktúry priečinkov počas obnovy!)" 109 | }, 110 | "execFinishCmd": { 111 | "label": "Príkaz po dokončení zálohy", 112 | "description": "Spustiť príkaz po dokončení zálohy" 113 | } 114 | }, 115 | "backupReadme": "# Joplin Backup\n\nTento priečinok obsahuje jednu alebo viac záloh údajov z aplikácie na zapisovanie poznámok Joplin.\n\nPozrite si [Dokumentáciu zálohovania](https://joplinapp.org/plugins/plugin/io.github.jackgruber.backup/#restore) pre informácie o tom, ako obnoviť túto zálohu.", 116 | "command": { 117 | "createBackup": "Vytvoriť zálohu" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/locales/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": { 3 | "backup": { 4 | "completed": "备份完成" 5 | }, 6 | "error": { 7 | "PluginUpgrade": "升级错误 %s: %s", 8 | "folderCreation": "创建文件夹时出错: %s", 9 | "ConfigureBackupPath": "请在Joplin > 工具 > 选项 > 备份 中配置备份路径", 10 | "PasswordMissMatch": "密码不正确!", 11 | "BackupPathDontExist": "备份路径 '%s' 不存在!", 12 | "BackupAlreadyRunning": "已经在进行备份!", 13 | "Backup": "备份为 %s 时出错: %s", 14 | "fileCopy": " %s 时复制文件/文件夹出错: %s", 15 | "deleteFile": " %s 时删除文件/文件夹出错: %s", 16 | "BackupSetNotSupportedChars": "“备份集名称”包含不允许的字符 ( %s )!" 17 | } 18 | }, 19 | "settings": { 20 | "path": { 21 | "label": "备份路径" 22 | }, 23 | "exportPath": { 24 | "label": "临时导出路径", 25 | "description": "用于导出的临时路径,笔记在被移动到“备份路径”前会存于此处" 26 | }, 27 | "backupRetention": { 28 | "label": "保留 x 个备份", 29 | "description": "如果设置了多个备份, 则会根据“备份集名称”在“备份路径”下创建多个子文件夹" 30 | }, 31 | "backupInterval": { 32 | "label": "备份间隔(以小时为单位)", 33 | "description": "0 = 停止自动备份" 34 | }, 35 | "onlyOnChange": { 36 | "label": "仅在更改时备份", 37 | "description": "仅当笔记发生更改时,在指定的备份间隔内创建备份" 38 | }, 39 | "usePassword": { 40 | "label": "使用密码保护备份", 41 | "description": "备份将通过加密的压缩文件进行保护" 42 | }, 43 | "password": { 44 | "label": "密码", 45 | "description": "如果输入了密码,则备份将受到密码保护" 46 | }, 47 | "passwordRepeat": { 48 | "label": "重复密码", 49 | "description": "请重复输入密码以确认" 50 | }, 51 | "fileLogLevel": { 52 | "label": "日志级别" 53 | }, 54 | "createSubfolder": { 55 | "label": "创建子文件夹", 56 | "description": "在配置的备份路径中创建一个子文件夹。请在“备份路径”中没有其他数据时才禁用!" 57 | }, 58 | "zipArchive": { 59 | "label": "创建压缩文件", 60 | "description": "如果设置了“使用密码保护备份”,则总是会创建压缩文件" 61 | }, 62 | "compressionLevel": { 63 | "label": "压缩等级", 64 | "description": "压缩文件的压缩等级" 65 | }, 66 | "backupSetName": { 67 | "label": "备份集名称", 68 | "description": "如果要保留多个备份,请为备份集指定名称" 69 | }, 70 | "backupPlugins": { 71 | "label": "备份插件", 72 | "description": "备份插件的JPL文件(不会备份插件设置!)" 73 | }, 74 | "exportFormat": { 75 | "label": "导出格式", 76 | "description": "笔记的备份格式" 77 | }, 78 | "singleJex": { 79 | "label": "生成单个JEX文件", 80 | "description": "为所有笔记本创建单个JEX文件(建议选中,以免丢失内部笔记链接和文件夹结构)" 81 | }, 82 | "execFinishCmd": { 83 | "label": "备份后执行命令", 84 | "description": "备份完成后所执行的命令/程序" 85 | } 86 | }, 87 | "command": { 88 | "createBackup": "创建备份" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 1, 3 | "id": "io.github.jackgruber.backup", 4 | "app_min_version": "2.1.3", 5 | "version": "1.4.4", 6 | "name": "Backup", 7 | "description": "Plugin to create manual and automatic backups.", 8 | "author": "JackGruber", 9 | "homepage_url": "https://github.com/JackGruber/joplin-plugin-backup/blob/master/README.md", 10 | "repository_url": "https://github.com/JackGruber/joplin-plugin-backup", 11 | "keywords": [ 12 | "backup", 13 | "jex", 14 | "export", 15 | "zip", 16 | "7zip", 17 | "encrypted", 18 | "archive" 19 | ], 20 | "categories": ["productivity", "files"], 21 | "screenshots": [ 22 | { 23 | "src": "img/main.png", 24 | "label": "Screenshot: Showing the basic settings" 25 | }, 26 | { 27 | "src": "img/showcase1.png", 28 | "label": "Screenshot: Showing the advanced settings" 29 | } 30 | ], 31 | "icons": { 32 | "256": "img/icon_256.png" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import joplin from "api"; 2 | import { SettingItemType, SettingItemSubType } from "api/types"; 3 | import { helper } from "./helper"; 4 | import { i18n } from "./Backup"; 5 | 6 | export namespace Settings { 7 | export async function register() { 8 | await joplin.settings.registerSection("backupSection", { 9 | label: i18n.__("settings.section.label"), 10 | iconName: "fas fa-archive", 11 | }); 12 | 13 | const joplinVersionInfo = await helper.joplinVersionInfo(); 14 | let pathSettings = null; 15 | pathSettings = { 16 | value: "", 17 | type: SettingItemType.String, 18 | section: "backupSection", 19 | public: true, 20 | label: i18n.__("settings.path.label"), 21 | description: i18n.__("settings.path.description"), 22 | }; 23 | 24 | let exportPathSettings = null; 25 | exportPathSettings = { 26 | value: "", 27 | type: SettingItemType.String, 28 | section: "backupSection", 29 | public: true, 30 | advanced: true, 31 | label: i18n.__("settings.exportPath.label"), 32 | description: i18n.__("settings.exportPath.description"), 33 | }; 34 | 35 | // Add DirectoryPath selector for newer Joplin versions 36 | if ( 37 | joplinVersionInfo !== null && 38 | (await helper.versionCompare(joplinVersionInfo.version, "2.10.4")) >= 0 39 | ) { 40 | pathSettings["subType"] = SettingItemSubType.DirectoryPath; 41 | exportPathSettings["subType"] = SettingItemSubType.DirectoryPath; 42 | } 43 | 44 | // Make export Format only onb Joplin > 2.9.12 public 45 | let exportFormatPublic = false; 46 | if ( 47 | joplinVersionInfo !== null && 48 | (await helper.versionCompare(joplinVersionInfo.version, "2.9.12")) >= 0 49 | ) { 50 | exportFormatPublic = true; 51 | } 52 | 53 | await joplin.settings.registerSettings({ 54 | path: pathSettings, 55 | backupRetention: { 56 | value: 1, 57 | minimum: 1, 58 | maximum: 999, 59 | type: SettingItemType.Int, 60 | section: "backupSection", 61 | public: true, 62 | label: i18n.__("settings.backupRetention.label"), 63 | description: i18n.__("settings.backupRetention.description"), 64 | }, 65 | backupInterval: { 66 | value: 24, 67 | minimum: 0, 68 | maximum: 999, 69 | type: SettingItemType.Int, 70 | section: "backupSection", 71 | public: true, 72 | label: i18n.__("settings.backupInterval.label"), 73 | description: i18n.__("settings.backupInterval.description"), 74 | }, 75 | onlyOnChange: { 76 | value: false, 77 | type: SettingItemType.Bool, 78 | section: "backupSection", 79 | public: true, 80 | label: i18n.__("settings.onlyOnChange.label"), 81 | description: i18n.__("settings.onlyOnChange.description"), 82 | }, 83 | usePassword: { 84 | value: false, 85 | type: SettingItemType.Bool, 86 | section: "backupSection", 87 | public: true, 88 | label: i18n.__("settings.usePassword.label"), 89 | description: i18n.__("settings.usePassword.description"), 90 | }, 91 | password: { 92 | value: "password", 93 | type: SettingItemType.String, 94 | section: "backupSection", 95 | public: true, 96 | secure: true, 97 | label: i18n.__("settings.password.label"), 98 | description: i18n.__("settings.password.description"), 99 | }, 100 | passwordRepeat: { 101 | value: "repeat12", 102 | type: SettingItemType.String, 103 | section: "backupSection", 104 | public: true, 105 | secure: true, 106 | label: i18n.__("settings.passwordRepeat.label"), 107 | description: i18n.__("settings.passwordRepeat.description"), 108 | }, 109 | lastBackup: { 110 | value: 0, 111 | type: SettingItemType.Int, 112 | section: "backupSection", 113 | public: false, 114 | label: "last backup run", 115 | }, 116 | fileLogLevel: { 117 | value: "error", 118 | type: SettingItemType.String, 119 | section: "backupSection", 120 | isEnum: true, 121 | public: true, 122 | label: i18n.__("settings.fileLogLevel.label"), 123 | description: i18n.__("settings.fileLogLevel.description"), 124 | options: { 125 | false: i18n.__("settings.fileLogLevel.value.false"), 126 | verbose: i18n.__("settings.fileLogLevel.value.verbose"), 127 | info: i18n.__("settings.fileLogLevel.value.info"), 128 | warn: i18n.__("settings.fileLogLevel.value.warn"), 129 | error: i18n.__("settings.fileLogLevel.value.error"), 130 | }, 131 | }, 132 | createSubfolder: { 133 | value: true, 134 | type: SettingItemType.Bool, 135 | section: "backupSection", 136 | public: true, 137 | advanced: true, 138 | label: i18n.__("settings.createSubfolder.label"), 139 | description: i18n.__("settings.createSubfolder.description", { 140 | backupPath: i18n.__("settings.path.label"), 141 | }), 142 | }, 143 | createSubfolderPerProfile: { 144 | value: false, 145 | type: SettingItemType.Bool, 146 | section: "backupSection", 147 | public: true, 148 | advanced: true, 149 | label: i18n.__("settings.createSubfolderPerProfile.label"), 150 | description: i18n.__("settings.createSubfolderPerProfile.description"), 151 | }, 152 | zipArchive: { 153 | value: "no", 154 | type: SettingItemType.String, 155 | section: "backupSection", 156 | isEnum: true, 157 | public: true, 158 | advanced: true, 159 | options: { 160 | no: i18n.__("settings.zipArchive.value.no"), 161 | yes: i18n.__("settings.zipArchive.value.yes"), 162 | yesone: i18n.__("settings.zipArchive.value.yesone"), 163 | }, 164 | label: i18n.__("settings.zipArchive.label"), 165 | description: i18n.__("settings.zipArchive.description"), 166 | }, 167 | compressionLevel: { 168 | value: 0, 169 | type: SettingItemType.Int, 170 | section: "backupSection", 171 | isEnum: true, 172 | public: true, 173 | advanced: true, 174 | options: { 175 | 0: i18n.__("settings.compressionLevel.value.copy"), 176 | 1: i18n.__("settings.compressionLevel.value.fastest"), 177 | 3: i18n.__("settings.compressionLevel.value.fast"), 178 | 5: i18n.__("settings.compressionLevel.value.normal"), 179 | 7: i18n.__("settings.compressionLevel.value.maximum"), 180 | 9: i18n.__("settings.compressionLevel.value.ultra"), 181 | }, 182 | label: i18n.__("settings.compressionLevel.label"), 183 | description: i18n.__("settings.compressionLevel.description"), 184 | }, 185 | exportPath: exportPathSettings, 186 | backupSetName: { 187 | value: "{YYYYMMDDHHmm}", 188 | type: SettingItemType.String, 189 | section: "backupSection", 190 | public: true, 191 | advanced: true, 192 | label: i18n.__("settings.backupSetName.label"), 193 | description: i18n.__("settings.backupSetName.description"), 194 | }, 195 | backupPlugins: { 196 | value: true, 197 | type: SettingItemType.Bool, 198 | section: "backupSection", 199 | public: true, 200 | advanced: true, 201 | label: i18n.__("settings.backupPlugins.label"), 202 | description: i18n.__("settings.backupPlugins.description"), 203 | }, 204 | exportFormat: { 205 | value: "jex", 206 | type: SettingItemType.String, 207 | section: "backupSection", 208 | isEnum: true, 209 | public: exportFormatPublic, 210 | advanced: true, 211 | options: { 212 | jex: "JEX", 213 | md_frontmatter: "MD Frontmatter", 214 | raw: "RAW", 215 | }, 216 | label: i18n.__("settings.exportFormat.label"), 217 | description: i18n.__("settings.exportFormat.description"), 218 | }, 219 | singleJexV2: { 220 | value: true, 221 | type: SettingItemType.Bool, 222 | section: "backupSection", 223 | public: true, 224 | advanced: true, 225 | label: i18n.__("settings.singleJex.label"), 226 | description: i18n.__("settings.singleJex.description"), 227 | }, 228 | singleJex: { 229 | value: false, 230 | type: SettingItemType.Bool, 231 | section: "backupSection", 232 | public: false, 233 | advanced: true, 234 | label: "Single JEX", 235 | description: "Old setting, for compatibility and upgrade only.", 236 | }, 237 | execFinishCmd: { 238 | value: "", 239 | type: SettingItemType.String, 240 | section: "backupSection", 241 | public: true, 242 | advanced: true, 243 | label: i18n.__("settings.execFinishCmd.label"), 244 | description: i18n.__("settings.execFinishCmd.description"), 245 | }, 246 | backupVersion: { 247 | value: 0, 248 | type: SettingItemType.Int, 249 | section: "backupSection", 250 | public: false, 251 | label: "Backup Version", 252 | }, 253 | backupInfo: { 254 | value: "[]", 255 | type: SettingItemType.String, 256 | section: "backupSection", 257 | public: false, 258 | label: "Backup info", 259 | }, 260 | }); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/sevenZip.ts: -------------------------------------------------------------------------------- 1 | // https://sevenzip.osdn.jp/chm/cmdline/exit_codes.htm 2 | // https://sevenzip.osdn.jp/chm/cmdline/commands/index.htm 3 | import * as _7z from "node-7z"; 4 | import * as sevenBin from "7zip-bin"; 5 | import * as path from "path"; 6 | import { exec } from "child_process"; 7 | import joplin from "api"; 8 | 9 | export let pathTo7zip = sevenBin.path7za; 10 | 11 | export namespace sevenZip { 12 | export async function updateBinPath() { 13 | pathTo7zip = path.join( 14 | await joplin.plugins.installationDir(), 15 | "7zip-bin", 16 | pathTo7zip 17 | ); 18 | } 19 | 20 | export async function setExecutionFlag() { 21 | if (process.platform !== "win32") { 22 | exec(`chmod +x ${pathTo7zip}`, (error, stdout, stderr) => { 23 | if (error) { 24 | console.error(`exec error: ${error}`); 25 | return; 26 | } 27 | }); 28 | } 29 | } 30 | 31 | async function addPassword( 32 | _7zOptions: any, 33 | password: string 34 | ): Promise { 35 | if (!_7zOptions.method) { 36 | _7zOptions.method = []; 37 | } 38 | if (password.indexOf('"') >= 0) { 39 | throw 'Password contains " (double quotes) https://github.com/quentinrossetti/node-7z/issues/132'; 40 | } 41 | 42 | _7zOptions.password = password; 43 | return _7zOptions; 44 | } 45 | 46 | export async function add( 47 | archive: string, 48 | src: string, 49 | password: string = null, 50 | options: Object = {} 51 | ): Promise { 52 | let _7zOptions: any = { $bin: pathTo7zip }; 53 | if (options) { 54 | _7zOptions = { ..._7zOptions, ...options }; 55 | } 56 | 57 | if (password !== null) { 58 | _7zOptions = await addPassword(_7zOptions, password); 59 | _7zOptions.method.push("he"); 60 | } 61 | 62 | const promise = new Promise((resolve, reject) => { 63 | const process = _7z.add(archive, src, _7zOptions); 64 | process.on("end", () => resolve(true)); 65 | process.on("error", reject); 66 | }); 67 | 68 | return await promise 69 | .then((data) => { 70 | return data; 71 | }) 72 | .catch((err) => { 73 | return err.message; 74 | }); 75 | } 76 | 77 | export async function list( 78 | archive: string, 79 | password: string = null, 80 | options: Object = {} 81 | ): Promise { 82 | let _7zOptions: any = { $bin: pathTo7zip }; 83 | if (options) { 84 | _7zOptions = { ..._7zOptions, ...options }; 85 | } 86 | 87 | if (password !== null) { 88 | _7zOptions = await addPassword(_7zOptions, password); 89 | } 90 | 91 | const promise = new Promise((resolve, reject) => { 92 | const files = []; 93 | const process = _7z.list(archive, _7zOptions); 94 | process.on("data", (file) => files.push(file)); 95 | process.on("end", () => resolve(files)); 96 | process.on("error", reject); 97 | }); 98 | 99 | return await promise 100 | .then((data) => { 101 | return data; 102 | }) 103 | .catch((err) => { 104 | return err.message; 105 | }); 106 | } 107 | 108 | export async function passwordProtected( 109 | archive: string, 110 | password: string = null, 111 | options: Object = {} 112 | ): Promise { 113 | let _7zOptions: any = { $bin: pathTo7zip }; 114 | if (options) { 115 | _7zOptions = { ..._7zOptions, ...options }; 116 | } 117 | 118 | _7zOptions = await addPassword(_7zOptions, "WrongPasswordForTesting"); 119 | 120 | const promise = new Promise((resolve, reject) => { 121 | const tests = []; 122 | const process = _7z.test(archive, _7zOptions); 123 | process.on("data", (data) => tests.push(data)); 124 | process.on("end", () => resolve(tests)); 125 | process.on("error", reject); 126 | }); 127 | 128 | return await promise 129 | .then((data) => { 130 | return false; 131 | }) 132 | .catch((err) => { 133 | if (archive == err.message) return true; 134 | else return err.message; 135 | }); 136 | } 137 | 138 | export async function test( 139 | archive: string, 140 | password: string = null, 141 | options: Object = {} 142 | ): Promise { 143 | let _7zOptions: any = { $bin: pathTo7zip }; 144 | if (options) { 145 | _7zOptions = { ..._7zOptions, ...options }; 146 | } 147 | 148 | if (password !== null) { 149 | _7zOptions = await addPassword(_7zOptions, password); 150 | } 151 | 152 | const promise = new Promise((resolve, reject) => { 153 | const tests = []; 154 | const process = _7z.test(archive, _7zOptions); 155 | process.on("data", (data) => tests.push(data)); 156 | process.on("end", () => resolve(tests)); 157 | process.on("error", reject); 158 | }); 159 | 160 | return await promise 161 | .then((data) => { 162 | return data; 163 | }) 164 | .catch((err) => { 165 | return err.message; 166 | }); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/webview.css: -------------------------------------------------------------------------------- 1 | #joplin-plugin-content { 2 | width: fit-content; 3 | background-color: var(--joplin-background-color); 4 | color: var(--joplin-color); 5 | } 6 | 7 | #backuperror { 8 | width: fit-content; 9 | display: block; 10 | flex-direction: column; 11 | min-width: 300px; 12 | overflow-wrap: break-word; 13 | font-size: var(--joplin-font-size); 14 | font-family: var(--joplin-font-family); 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "jsx": "react", 7 | "allowJs": true, 8 | "baseUrl": "." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file is used to build the plugin file (.jpl) and plugin info (.json). It 3 | // is recommended not to edit this file as it would be overwritten when updating 4 | // the plugin framework. If you do make some changes, consider using an external 5 | // JS file and requiring it here to minimize the changes. That way when you 6 | // update, you can easily restore the functionality you've added. 7 | // ----------------------------------------------------------------------------- 8 | 9 | /* eslint-disable no-console */ 10 | 11 | const path = require('path'); 12 | const crypto = require('crypto'); 13 | const fs = require('fs-extra'); 14 | const chalk = require('chalk'); 15 | const CopyPlugin = require('copy-webpack-plugin'); 16 | const tar = require('tar'); 17 | const glob = require('glob'); 18 | const execSync = require('child_process').execSync; 19 | const allPossibleCategories = require('@joplin/lib/pluginCategories.json'); 20 | 21 | const rootDir = path.resolve(__dirname); 22 | const userConfigFilename = './plugin.config.json'; 23 | const userConfigPath = path.resolve(rootDir, userConfigFilename); 24 | const distDir = path.resolve(rootDir, 'dist'); 25 | const srcDir = path.resolve(rootDir, 'src'); 26 | const publishDir = path.resolve(rootDir, 'publish'); 27 | 28 | const userConfig = { extraScripts: [], ...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}) }; 29 | 30 | const manifestPath = `${srcDir}/manifest.json`; 31 | const packageJsonPath = `${rootDir}/package.json`; 32 | const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp']; 33 | const manifest = readManifest(manifestPath); 34 | const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`); 35 | const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`); 36 | 37 | const { builtinModules } = require('node:module'); 38 | 39 | // Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in 40 | // node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules. 41 | // We don't need to polyfill because the plugins run in Electron's Node environment. 42 | const moduleFallback = {}; 43 | for (const moduleName of builtinModules) { 44 | moduleFallback[moduleName] = false; 45 | } 46 | 47 | const getPackageJson = () => { 48 | return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 49 | }; 50 | 51 | function validatePackageJson() { 52 | const content = getPackageJson(); 53 | if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) { 54 | console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`)); 55 | } 56 | 57 | if (!content.keywords || content.keywords.indexOf('joplin-plugin') < 0) { 58 | console.warn(chalk.yellow(`WARNING: To publish the plugin, the package keywords should include "joplin-plugin" (found "${JSON.stringify(content.keywords)}") in ${packageJsonPath}`)); 59 | } 60 | 61 | if (content.scripts && content.scripts.postinstall) { 62 | console.warn(chalk.yellow(`WARNING: package.json contains a "postinstall" script. It is recommended to use a "prepare" script instead so that it is executed before publish. In ${packageJsonPath}`)); 63 | } 64 | } 65 | 66 | function fileSha256(filePath) { 67 | const content = fs.readFileSync(filePath); 68 | return crypto.createHash('sha256').update(content).digest('hex'); 69 | } 70 | 71 | function currentGitInfo() { 72 | try { 73 | let branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim(); 74 | const commit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim(); 75 | if (branch === 'HEAD') branch = 'master'; 76 | return `${branch}:${commit}`; 77 | } catch (error) { 78 | const messages = error.message ? error.message.split('\n') : ['']; 79 | console.info(chalk.cyan('Could not get git commit (not a git repo?):', messages[0].trim())); 80 | console.info(chalk.cyan('Git information will not be stored in plugin info file')); 81 | return ''; 82 | } 83 | } 84 | 85 | function validateCategories(categories) { 86 | if (!categories) return null; 87 | if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed'); 88 | // eslint-disable-next-line github/array-foreach -- Old code before rule was applied 89 | categories.forEach(category => { 90 | if (!allPossibleCategories.map(category => { return category.name; }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n${allPossibleCategories.map(category => { return category.name; })}\n`); 91 | }); 92 | } 93 | 94 | function validateScreenshots(screenshots) { 95 | if (!screenshots) return null; 96 | for (const screenshot of screenshots) { 97 | if (!screenshot.src) throw new Error('You must specify a src for each screenshot'); 98 | 99 | // Avoid attempting to download and verify URL screenshots. 100 | if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) { 101 | continue; 102 | } 103 | 104 | const screenshotType = screenshot.src.split('.').pop(); 105 | if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`); 106 | 107 | const screenshotPath = path.resolve(rootDir, screenshot.src); 108 | 109 | // Max file size is 1MB 110 | const fileMaxSize = 1024; 111 | const fileSize = fs.statSync(screenshotPath).size / 1024; 112 | if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`); 113 | } 114 | } 115 | 116 | function readManifest(manifestPath) { 117 | const content = fs.readFileSync(manifestPath, 'utf8'); 118 | const output = JSON.parse(content); 119 | if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`); 120 | validateCategories(output.categories); 121 | validateScreenshots(output.screenshots); 122 | return output; 123 | } 124 | 125 | function createPluginArchive(sourceDir, destPath) { 126 | const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true }) 127 | .map(f => f.substr(sourceDir.length + 1)); 128 | 129 | if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty'); 130 | fs.removeSync(destPath); 131 | 132 | tar.create( 133 | { 134 | strict: true, 135 | portable: true, 136 | file: destPath, 137 | cwd: sourceDir, 138 | sync: true, 139 | }, 140 | distFiles, 141 | ); 142 | 143 | console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`)); 144 | } 145 | 146 | const writeManifest = (manifestPath, content) => { 147 | fs.writeFileSync(manifestPath, JSON.stringify(content, null, '\t'), 'utf8'); 148 | }; 149 | 150 | function createPluginInfo(manifestPath, destPath, jplFilePath) { 151 | const contentText = fs.readFileSync(manifestPath, 'utf8'); 152 | const content = JSON.parse(contentText); 153 | content._publish_hash = `sha256:${fileSha256(jplFilePath)}`; 154 | content._publish_commit = currentGitInfo(); 155 | writeManifest(destPath, content); 156 | } 157 | 158 | function onBuildCompleted() { 159 | try { 160 | fs.removeSync(path.resolve(publishDir, 'index.js')); 161 | createPluginArchive(distDir, pluginArchiveFilePath); 162 | createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath); 163 | validatePackageJson(); 164 | } catch (error) { 165 | console.error(chalk.red(error.message)); 166 | } 167 | } 168 | 169 | const baseConfig = { 170 | mode: 'production', 171 | target: 'node', 172 | stats: 'errors-only', 173 | module: { 174 | rules: [ 175 | { 176 | test: /\.tsx?$/, 177 | use: 'ts-loader', 178 | exclude: /node_modules/, 179 | }, 180 | ], 181 | }, 182 | 183 | // 7zip-bin uses __dirname to determine the path to bundled binaries. 184 | // In Joplin, __dirname points to the plugin directory (for example 185 | // /tmp/.mount_Joplinf4CQVb/resources/app.asar/services/plugins/), 186 | // which is not where 7zip binaries are stored. 187 | node: { 188 | // Makes __dirname evaluate to '/' 189 | __dirname: 'mock', 190 | }, 191 | }; 192 | 193 | const pluginConfig = { ...baseConfig, entry: './src/index.ts', 194 | resolve: { 195 | alias: { 196 | api: path.resolve(__dirname, 'api'), 197 | }, 198 | fallback: moduleFallback, 199 | // JSON files can also be required from scripts so we include this. 200 | // https://github.com/joplin/plugin-bibtex/pull/2 201 | extensions: ['.js', '.tsx', '.ts', '.json'], 202 | }, 203 | output: { 204 | filename: 'index.js', 205 | path: distDir, 206 | }, 207 | plugins: [ 208 | new CopyPlugin({ 209 | patterns: [ 210 | { 211 | from: '**/*', 212 | context: path.resolve(__dirname, 'node_modules','7zip-bin'), 213 | to: path.resolve(__dirname, 'dist/7zip-bin/'), 214 | }, 215 | ] 216 | }), 217 | new CopyPlugin({ 218 | patterns: [ 219 | { 220 | from: '**/*', 221 | context: path.resolve(__dirname, 'src'), 222 | to: path.resolve(__dirname, 'dist'), 223 | globOptions: { 224 | ignore: [ 225 | // All TypeScript files are compiled to JS and 226 | // already copied into /dist so we don't copy them. 227 | '**/*.ts', 228 | '**/*.tsx', 229 | ], 230 | }, 231 | }, 232 | ], 233 | }), 234 | ] }; 235 | 236 | const extraScriptConfig = { ...baseConfig, resolve: { 237 | alias: { 238 | api: path.resolve(__dirname, 'api'), 239 | }, 240 | fallback: moduleFallback, 241 | extensions: ['.js', '.tsx', '.ts', '.json'], 242 | } }; 243 | 244 | const createArchiveConfig = { 245 | stats: 'errors-only', 246 | entry: './dist/index.js', 247 | resolve: { 248 | fallback: moduleFallback, 249 | }, 250 | output: { 251 | filename: 'index.js', 252 | path: publishDir, 253 | }, 254 | plugins: [{ 255 | apply(compiler) { 256 | compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted); 257 | }, 258 | }], 259 | }; 260 | 261 | function resolveExtraScriptPath(name) { 262 | const relativePath = `./src/${name}`; 263 | 264 | const fullPath = path.resolve(`${rootDir}/${relativePath}`); 265 | if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`); 266 | 267 | const s = name.split('.'); 268 | s.pop(); 269 | const nameNoExt = s.join('.'); 270 | 271 | return { 272 | entry: relativePath, 273 | output: { 274 | filename: `${nameNoExt}.js`, 275 | path: distDir, 276 | library: 'default', 277 | libraryTarget: 'commonjs', 278 | libraryExport: 'default', 279 | }, 280 | }; 281 | } 282 | 283 | function buildExtraScriptConfigs(userConfig) { 284 | if (!userConfig.extraScripts.length) return []; 285 | 286 | const output = []; 287 | 288 | for (const scriptName of userConfig.extraScripts) { 289 | const scriptPaths = resolveExtraScriptPath(scriptName); 290 | output.push({ ...extraScriptConfig, entry: scriptPaths.entry, 291 | output: scriptPaths.output }); 292 | } 293 | 294 | return output; 295 | } 296 | 297 | const increaseVersion = version => { 298 | try { 299 | const s = version.split('.'); 300 | const d = Number(s[s.length - 1]) + 1; 301 | s[s.length - 1] = `${d}`; 302 | return s.join('.'); 303 | } catch (error) { 304 | error.message = `Could not parse version number: ${version}: ${error.message}`; 305 | throw error; 306 | } 307 | }; 308 | 309 | const updateVersion = () => { 310 | const packageJson = getPackageJson(); 311 | packageJson.version = increaseVersion(packageJson.version); 312 | fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8'); 313 | 314 | const manifest = readManifest(manifestPath); 315 | manifest.version = increaseVersion(manifest.version); 316 | writeManifest(manifestPath, manifest); 317 | 318 | if (packageJson.version !== manifest.version) { 319 | console.warn(chalk.yellow(`Version numbers have been updated but they do not match: package.json (${packageJson.version}), manifest.json (${manifest.version}). Set them to the required values to get them in sync.`)); 320 | } 321 | }; 322 | 323 | function main(environ) { 324 | const configName = environ['joplin-plugin-config']; 325 | if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag'); 326 | 327 | // Webpack configurations run in parallel, while we need them to run in 328 | // sequence, and to do that it seems the only way is to run webpack multiple 329 | // times, with different config each time. 330 | 331 | const configs = { 332 | // Builds the main src/index.ts and copy the extra content from /src to 333 | // /dist including scripts, CSS and any other asset. 334 | buildMain: [pluginConfig], 335 | 336 | // Builds the extra scripts as defined in plugin.config.json. When doing 337 | // so, some JavaScript files that were copied in the previous might be 338 | // overwritten here by the compiled version. This is by design. The 339 | // result is that JS files that don't need compilation, are simply 340 | // copied to /dist, while those that do need it are correctly compiled. 341 | buildExtraScripts: buildExtraScriptConfigs(userConfig), 342 | 343 | // Ths config is for creating the .jpl, which is done via the plugin, so 344 | // it doesn't actually need an entry and output, however webpack won't 345 | // run without this. So we give it an entry that we know is going to 346 | // exist and output in the publish dir. Then the plugin will delete this 347 | // temporary file before packaging the plugin. 348 | createArchive: [createArchiveConfig], 349 | }; 350 | 351 | // If we are running the first config step, we clean up and create the build 352 | // directories. 353 | if (configName === 'buildMain') { 354 | fs.removeSync(distDir); 355 | fs.removeSync(publishDir); 356 | fs.mkdirpSync(publishDir); 357 | } 358 | 359 | if (configName === 'updateVersion') { 360 | updateVersion(); 361 | return []; 362 | } 363 | 364 | return configs[configName]; 365 | } 366 | 367 | 368 | module.exports = (env) => { 369 | let exportedConfigs = []; 370 | 371 | try { 372 | exportedConfigs = main(env); 373 | } catch (error) { 374 | console.error(error.message); 375 | process.exit(1); 376 | } 377 | 378 | if (!exportedConfigs.length) { 379 | // Nothing to do - for example where there are no external scripts to 380 | // compile. 381 | process.exit(0); 382 | } 383 | 384 | return exportedConfigs; 385 | }; 386 | --------------------------------------------------------------------------------