You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-02-19 08:38:26 +02:00
Compare commits
90 Commits
android-v3
...
transcribe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a747996d8c | ||
|
|
0877d6e9cd | ||
|
|
66aa47a5ca | ||
|
|
535158e07a | ||
|
|
5776aff0df | ||
|
|
59e38bac45 | ||
|
|
e581c4cd67 | ||
|
|
e6604e369f | ||
|
|
60f76afa54 | ||
|
|
fc2f0994fa | ||
|
|
908d568f6e | ||
|
|
45b44b962e | ||
|
|
ced97edb52 | ||
|
|
ed620b7ec0 | ||
|
|
44f77fa04b | ||
|
|
730f7074fd | ||
|
|
3dc6c6d272 | ||
|
|
6edc74ed3a | ||
|
|
0192033845 | ||
|
|
7285270df3 | ||
|
|
ab05bb8cc5 | ||
|
|
887084a6a0 | ||
|
|
a2f6906668 | ||
|
|
5ad4c31b44 | ||
|
|
b099840f97 | ||
|
|
60fec1ce69 | ||
|
|
231c9a2343 | ||
|
|
a72ea3f2ae | ||
|
|
9f89fee494 | ||
|
|
2c3e6d5e11 | ||
|
|
dbf7b6195e | ||
|
|
365729c759 | ||
|
|
34dfcb9668 | ||
|
|
32585576a9 | ||
|
|
ef32d53f72 | ||
|
|
232ff19824 | ||
|
|
d3d32fc072 | ||
|
|
a7ac7535b7 | ||
|
|
cdb1de52eb | ||
|
|
e7a4227f6f | ||
|
|
4d9c161c43 | ||
|
|
8eaca2edf9 | ||
|
|
35bb52302f | ||
|
|
e181fef1ae | ||
|
|
f6c8f5b6ca | ||
|
|
0ec4571540 | ||
|
|
7bd8255e26 | ||
|
|
a7050f678e | ||
|
|
c4895bdb7b | ||
|
|
452a3663b9 | ||
|
|
f5843e4651 | ||
|
|
9bdf5f4a63 | ||
|
|
e3028e39fe | ||
|
|
14284be1c2 | ||
|
|
bfa0a80772 | ||
|
|
5b99c40fbd | ||
|
|
adab482fb1 | ||
|
|
68073e4ad8 | ||
|
|
38e1ede8b4 | ||
|
|
4afa3aa1ee | ||
|
|
413db88fc9 | ||
|
|
20d88a2add | ||
|
|
0da94b2f8e | ||
|
|
6d306267d8 | ||
|
|
adb1367dcc | ||
|
|
1d9864fae7 | ||
|
|
a5a1634fb0 | ||
|
|
fdaf9c4b5d | ||
|
|
767c9cd587 | ||
|
|
266b177047 | ||
|
|
1d4478c28e | ||
|
|
47f15b6c32 | ||
|
|
9edfa0c2b0 | ||
|
|
9d4506a6f8 | ||
|
|
4cd9501165 | ||
|
|
335ba15784 | ||
|
|
f88c1df7ff | ||
|
|
a6c42898df | ||
|
|
cbc9b452e1 | ||
|
|
3739fac751 | ||
|
|
7a745b872a | ||
|
|
97a18f722d | ||
|
|
554e6efaab | ||
|
|
cd7af20bc1 | ||
|
|
4346616cae | ||
|
|
ef646adafa | ||
|
|
4ce47807b1 | ||
|
|
9b0bc4d600 | ||
|
|
91b8e4d34d | ||
|
|
9e9bd662dc |
95
.coderabbit.yaml
Normal file
95
.coderabbit.yaml
Normal file
@@ -0,0 +1,95 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
language: "en-GB"
|
||||
reviews:
|
||||
high_level_summary: false
|
||||
estimate_code_review_effort: false
|
||||
poem: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
ignore_usernames:
|
||||
- "renovate[bot]"
|
||||
auto_apply_labels: true
|
||||
labeling_instructions:
|
||||
- label: "accessibility"
|
||||
instructions: "Apply when the PR contains changes related to accessibility, screen readers, keyboard navigation, or ARIA attributes."
|
||||
- label: "android"
|
||||
instructions: "Apply when the PR contains changes specific to the Android platform or Android app."
|
||||
- label: "api"
|
||||
instructions: "Apply when the PR modifies the Joplin API, REST endpoints, or API-related code."
|
||||
- label: "bug"
|
||||
instructions: "Apply when the PR fixes a bug or unexpected behaviour."
|
||||
- label: "ci"
|
||||
instructions: "Apply when the PR modifies CI/CD configuration, GitHub Actions workflows, or build pipelines."
|
||||
- label: "cli"
|
||||
instructions: "Apply when the PR contains changes specific to the Joplin CLI (command-line) application."
|
||||
- label: "clipper"
|
||||
instructions: "Apply when the PR contains changes to the Joplin Web Clipper browser extension."
|
||||
- label: "database"
|
||||
instructions: "Apply when the PR modifies database schema, migrations, or database-related logic."
|
||||
- label: "desktop"
|
||||
instructions: "Apply when the PR contains changes specific to the Joplin desktop (Electron) application."
|
||||
- label: "documentation"
|
||||
instructions: "Apply when the PR adds or updates documentation, README files, or code comments."
|
||||
- label: "draw"
|
||||
instructions: "Apply when the PR contains changes related to the drawing or sketching feature."
|
||||
- label: "editor"
|
||||
instructions: "Apply when the PR contains changes to the note editor (CodeMirror, TinyMCE, or the editor infrastructure)."
|
||||
- label: "enhancement"
|
||||
instructions: "Apply when the PR adds a new feature or improves existing functionality (not a bug fix)."
|
||||
- label: "export"
|
||||
instructions: "Apply when the PR contains changes to export functionality (PDF, HTML, JEX, etc.)."
|
||||
- label: "import"
|
||||
instructions: "Apply when the PR contains changes to import functionality (Evernote, Markdown, etc.)."
|
||||
- label: "iOS"
|
||||
instructions: "Apply when the PR contains changes specific to the iOS platform or iOS app."
|
||||
- label: "linux"
|
||||
instructions: "Apply when the PR contains changes specific to Linux."
|
||||
- label: "linux/wayland"
|
||||
instructions: "Apply when the PR contains changes specific to Linux Wayland."
|
||||
- label: "macOS"
|
||||
instructions: "Apply when the PR contains changes specific to macOS."
|
||||
- label: "markdown-editor"
|
||||
instructions: "Apply when the PR contains changes to the Markdown editor or Markdown rendering."
|
||||
- label: "mobile"
|
||||
instructions: "Apply when the PR contains changes to the mobile app (iOS or Android)."
|
||||
- label: "multi-window"
|
||||
instructions: "Apply when the PR contains changes related to multi-window support."
|
||||
- label: "OCR"
|
||||
instructions: "Apply when the PR contains changes related to OCR (optical character recognition) functionality."
|
||||
- label: "performance"
|
||||
instructions: "Apply when the PR improves performance, reduces memory usage, or optimises speed."
|
||||
- label: "plugins"
|
||||
instructions: "Apply when the PR contains changes to the plugin system, plugin API, or specific plugins."
|
||||
- label: "Regression"
|
||||
instructions: "Apply when the linked issue, if any, has the Regression label."
|
||||
- label: "renderer"
|
||||
instructions: "Apply when the PR contains changes to the note renderer or how notes are displayed."
|
||||
- label: "search"
|
||||
instructions: "Apply when the PR contains changes to search functionality."
|
||||
- label: "security"
|
||||
instructions: "Apply when the PR addresses a security vulnerability or improves security."
|
||||
- label: "server"
|
||||
instructions: "Apply when the PR contains changes to Joplin Server."
|
||||
- label: "Sharing"
|
||||
instructions: "Apply when the PR contains changes to note or notebook sharing features."
|
||||
- label: "sync"
|
||||
instructions: "Apply when the PR contains changes to synchronisation logic or sync targets."
|
||||
- label: "tags"
|
||||
instructions: "Apply when the PR contains changes to tag management or tagging functionality."
|
||||
- label: "transcribe"
|
||||
instructions: "Apply when the PR contains changes to audio transcription functionality."
|
||||
- label: "translation"
|
||||
instructions: "Apply when the PR adds or updates translations or localisation strings."
|
||||
- label: "Voice typing"
|
||||
instructions: "Apply when the PR contains changes to voice typing functionality."
|
||||
- label: "web"
|
||||
instructions: "Apply when the PR contains changes to the Joplin web application or web-related features."
|
||||
- label: "windows"
|
||||
instructions: "Apply when the PR contains changes specific to Windows."
|
||||
knowledge_base:
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns:
|
||||
- "readme/dev/coding_style.md"
|
||||
- "readme/dev/index.md"
|
||||
@@ -208,7 +208,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
packages/app-desktop/gui/DialogTitle.js
|
||||
@@ -248,6 +247,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js
|
||||
@@ -269,6 +269,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
@@ -394,7 +395,9 @@ packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/ProfileEditor.js
|
||||
packages/app-desktop/gui/PromptDialog.test.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/QuitSyncDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js
|
||||
@@ -523,7 +526,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useDocument.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -623,10 +625,12 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
|
||||
packages/app-desktop/utils/customProtocols/constants.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.content.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.plugins.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
|
||||
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
|
||||
packages/app-desktop/utils/getAssetPath.js
|
||||
packages/app-desktop/utils/initReact.js
|
||||
packages/app-desktop/utils/initializeCommandService.js
|
||||
packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
@@ -729,6 +733,8 @@ packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.test.js
|
||||
packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.js
|
||||
packages/app-mobile/components/SafeAreaView.js
|
||||
packages/app-mobile/components/ScreenHeader/Menu.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
@@ -859,6 +865,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/TextWrapCalculator.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
@@ -976,6 +983,7 @@ packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initReact.js
|
||||
packages/app-mobile/utils/initReact.web.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
@@ -1256,6 +1264,7 @@ packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/TaskQueue.js
|
||||
packages/lib/WebDavApi.js
|
||||
packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
@@ -1279,6 +1288,7 @@ packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleAllFolders.test.js
|
||||
packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/Dialog.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick.js
|
||||
@@ -1305,6 +1315,7 @@ packages/lib/components/shared/reduxSharedMiddleware.js
|
||||
packages/lib/components/shared/side-menu-shared.test.js
|
||||
packages/lib/components/shared/side-menu-shared.js
|
||||
packages/lib/database-driver-better-sqlite.js
|
||||
packages/lib/database-driver.js
|
||||
packages/lib/database.js
|
||||
packages/lib/debug/DebugService.js
|
||||
packages/lib/determineBaseAppDirs.js
|
||||
@@ -1327,6 +1338,7 @@ packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/dom/useDocument.js
|
||||
packages/lib/hooks/plugins/usePlugin.js
|
||||
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
@@ -1524,6 +1536,7 @@ packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.electron.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
packages/lib/services/noteList/checkboxPieCss.js
|
||||
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
|
||||
packages/lib/services/noteList/defaultListRenderer.js
|
||||
packages/lib/services/noteList/defaultMultiColumnsRenderer.js
|
||||
@@ -1710,6 +1723,7 @@ packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
packages/lib/string-utils.js
|
||||
packages/lib/testing/dom-test-environment.js
|
||||
packages/lib/testing/plugins/createTestPlugin.js
|
||||
packages/lib/testing/share/makeMockShareInvitation.js
|
||||
packages/lib/testing/share/mockShareService.js
|
||||
@@ -1857,14 +1871,18 @@ packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/Fuzzer.js
|
||||
packages/tools/fuzzer/cli.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/ipc/Client.js
|
||||
packages/tools/fuzzer/ipc/ClientPool.js
|
||||
packages/tools/fuzzer/ipc/Server.js
|
||||
packages/tools/fuzzer/model/ActionTracker.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/model/NoteRecord.js
|
||||
packages/tools/fuzzer/model/ResourceRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/model/Serializable.js
|
||||
packages/tools/fuzzer/model/types.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
@@ -1949,6 +1967,7 @@ packages/tools/website/utils/parser.js
|
||||
packages/tools/website/utils/pressCarousel.js
|
||||
packages/tools/website/utils/processTranslations.js
|
||||
packages/tools/website/utils/render.js
|
||||
packages/tools/website/utils/supportedLocales.js
|
||||
packages/tools/website/utils/types.js
|
||||
packages/whisper-voice-typing/src/index.js
|
||||
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
|
||||
|
||||
@@ -214,6 +214,7 @@ module.exports = {
|
||||
'packages/tools/**',
|
||||
'packages/app-mobile/tools/**',
|
||||
'packages/app-desktop/tools/**',
|
||||
'packages/transcribe/src/tools/**',
|
||||
],
|
||||
'rules': {
|
||||
'no-console': 'off',
|
||||
|
||||
2
.github/workflows/build-macos-m1.yml
vendored
2
.github/workflows/build-macos-m1.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
# See github-action-main.yml for explanation
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Set Publish Flag
|
||||
run: |
|
||||
|
||||
@@ -72,4 +72,4 @@ runs:
|
||||
# Ref: https://github.com/nodejs/node-gyp/issues/2869
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
python-version: '3.14'
|
||||
|
||||
35
.gitignore
vendored
35
.gitignore
vendored
@@ -181,7 +181,6 @@ packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
packages/app-desktop/gui/Dialog.js
|
||||
packages/app-desktop/gui/DialogButtonRow.js
|
||||
packages/app-desktop/gui/DialogButtonRow/useKeyboardHandler.js
|
||||
packages/app-desktop/gui/DialogTitle.js
|
||||
@@ -221,6 +220,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/normalizeAccelerator.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.test.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.js
|
||||
@@ -242,6 +242,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
@@ -367,7 +368,9 @@ packages/app-desktop/gui/PopupNotification/PopupNotificationList.js
|
||||
packages/app-desktop/gui/PopupNotification/PopupNotificationProvider.js
|
||||
packages/app-desktop/gui/PopupNotification/types.js
|
||||
packages/app-desktop/gui/ProfileEditor.js
|
||||
packages/app-desktop/gui/PromptDialog.test.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/QuitSyncDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js
|
||||
@@ -496,7 +499,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js
|
||||
packages/app-desktop/gui/dialogs.js
|
||||
packages/app-desktop/gui/hooks/useDocument.js
|
||||
packages/app-desktop/gui/hooks/useEffectDebugger.js
|
||||
packages/app-desktop/gui/hooks/useElementHeight.js
|
||||
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
|
||||
@@ -596,10 +598,12 @@ packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
|
||||
packages/app-desktop/utils/customProtocols/constants.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.content.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.plugins.test.js
|
||||
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
|
||||
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
|
||||
packages/app-desktop/utils/getAssetPath.js
|
||||
packages/app-desktop/utils/initReact.js
|
||||
packages/app-desktop/utils/initializeCommandService.js
|
||||
packages/app-desktop/utils/isSafeToOpen.test.js
|
||||
packages/app-desktop/utils/isSafeToOpen.js
|
||||
@@ -702,6 +706,8 @@ packages/app-mobile/components/NoteList.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
|
||||
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
|
||||
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
|
||||
packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.test.js
|
||||
packages/app-mobile/components/ProfileSwitcher/utils/deleteProfile.js
|
||||
packages/app-mobile/components/SafeAreaView.js
|
||||
packages/app-mobile/components/ScreenHeader/Menu.js
|
||||
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
|
||||
@@ -832,6 +838,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/Notes/TextWrapCalculator.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchBar.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
@@ -949,6 +956,7 @@ packages/app-mobile/utils/image/fileToImage.web.js
|
||||
packages/app-mobile/utils/image/getImageDimensions.js
|
||||
packages/app-mobile/utils/image/resizeImage.js
|
||||
packages/app-mobile/utils/initReact.js
|
||||
packages/app-mobile/utils/initReact.web.js
|
||||
packages/app-mobile/utils/initializeCommandService.js
|
||||
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
|
||||
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
|
||||
@@ -1229,6 +1237,7 @@ packages/lib/SyncTargetOneDrive.js
|
||||
packages/lib/SyncTargetRegistry.js
|
||||
packages/lib/Synchronizer.js
|
||||
packages/lib/TaskQueue.js
|
||||
packages/lib/WebDavApi.js
|
||||
packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
@@ -1252,6 +1261,7 @@ packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleAllFolders.test.js
|
||||
packages/lib/commands/toggleAllFolders.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/Dialog.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteEditor/WarningBanner/onRichTextDismissLinkClick.js
|
||||
@@ -1278,6 +1288,7 @@ packages/lib/components/shared/reduxSharedMiddleware.js
|
||||
packages/lib/components/shared/side-menu-shared.test.js
|
||||
packages/lib/components/shared/side-menu-shared.js
|
||||
packages/lib/database-driver-better-sqlite.js
|
||||
packages/lib/database-driver.js
|
||||
packages/lib/database.js
|
||||
packages/lib/debug/DebugService.js
|
||||
packages/lib/determineBaseAppDirs.js
|
||||
@@ -1300,6 +1311,7 @@ packages/lib/fsDriver.test.js
|
||||
packages/lib/geolocation-node.js
|
||||
packages/lib/getAppName.test.js
|
||||
packages/lib/getAppName.js
|
||||
packages/lib/hooks/dom/useDocument.js
|
||||
packages/lib/hooks/plugins/usePlugin.js
|
||||
packages/lib/hooks/plugins/useVisiblePluginEditorViewIds.js
|
||||
packages/lib/hooks/useAsyncEffect.js
|
||||
@@ -1497,6 +1509,7 @@ packages/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.electron.js
|
||||
packages/lib/services/keychain/KeychainServiceDriver.node.js
|
||||
packages/lib/services/keychain/KeychainServiceDriverBase.js
|
||||
packages/lib/services/noteList/checkboxPieCss.js
|
||||
packages/lib/services/noteList/defaultLeftToRightListRenderer.js
|
||||
packages/lib/services/noteList/defaultListRenderer.js
|
||||
packages/lib/services/noteList/defaultMultiColumnsRenderer.js
|
||||
@@ -1683,6 +1696,7 @@ packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
packages/lib/string-utils.js
|
||||
packages/lib/testing/dom-test-environment.js
|
||||
packages/lib/testing/plugins/createTestPlugin.js
|
||||
packages/lib/testing/share/makeMockShareInvitation.js
|
||||
packages/lib/testing/share/mockShareService.js
|
||||
@@ -1830,14 +1844,18 @@ packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/Fuzzer.js
|
||||
packages/tools/fuzzer/cli.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/ipc/Client.js
|
||||
packages/tools/fuzzer/ipc/ClientPool.js
|
||||
packages/tools/fuzzer/ipc/Server.js
|
||||
packages/tools/fuzzer/model/ActionTracker.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/model/NoteRecord.js
|
||||
packages/tools/fuzzer/model/ResourceRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/model/Serializable.js
|
||||
packages/tools/fuzzer/model/types.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
@@ -1922,6 +1940,7 @@ packages/tools/website/utils/parser.js
|
||||
packages/tools/website/utils/pressCarousel.js
|
||||
packages/tools/website/utils/processTranslations.js
|
||||
packages/tools/website/utils/render.js
|
||||
packages/tools/website/utils/supportedLocales.js
|
||||
packages/tools/website/utils/types.js
|
||||
packages/whisper-voice-typing/src/index.js
|
||||
packages/whisper-voice-typing/src/specs/Whisper.nitro.js
|
||||
|
||||
@@ -658,13 +658,84 @@ footer .bottom-links-row p {
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.language-switcher > button {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: #0557ba;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.language-switcher > button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.language-switcher .dropdown-menu {
|
||||
min-width: 100px;
|
||||
padding: 5px 0 !important;
|
||||
margin: 0 !important;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.language-switcher .dropdown-menu li {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.language-switcher .dropdown-item {
|
||||
color: #333 !important;
|
||||
padding: 8px 15px !important;
|
||||
margin: 0 !important;
|
||||
display: block;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.language-switcher .dropdown-item.active {
|
||||
background-color: #0557ba !important;
|
||||
color: #fff !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.language-switcher .dropdown-item:hover:not(.active) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* Language switcher on front page (blue background) */
|
||||
.navbar-frontpage .language-switcher > button {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Mobile language section */
|
||||
.menu-mobile-language {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.mobile-menu-language-label {
|
||||
color: #90b1d9;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.mobile-language-link {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
padding: 5px 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.mobile-language-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.joplin-cloud-feature-list .feature-description {
|
||||
|
||||
BIN
Assets/WebsiteAssets/images/news/20260210-hmd-joplin-logo.png
Normal file
BIN
Assets/WebsiteAssets/images/news/20260210-hmd-joplin-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
Assets/WebsiteAssets/images/news/20260210-hmd-terra-m.jpg
Normal file
BIN
Assets/WebsiteAssets/images/news/20260210-hmd-terra-m.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
@@ -124,7 +124,52 @@ async function setupDownloadPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Supported locale path prefixes (language code -> URL path)
|
||||
// Most languages use their code directly (fr, de), with exceptions mapped here
|
||||
const localePathOverrides = {
|
||||
'zh': 'cn',
|
||||
};
|
||||
|
||||
// List of supported language codes
|
||||
const supportedLanguages = ['fr', 'de', 'zh'];
|
||||
|
||||
function getLocalePath(langCode) {
|
||||
const pathPrefix = localePathOverrides[langCode] || langCode;
|
||||
return '/' + pathPrefix;
|
||||
}
|
||||
|
||||
function setupLocaleRedirect() {
|
||||
// Only redirect on the front page (root path or index.html)
|
||||
const path = window.location.pathname;
|
||||
const isRootPage = path === '/' || path === '/index.html' || path === '';
|
||||
if (!isRootPage) return;
|
||||
|
||||
// Check if user has explicitly chosen to stay on current locale
|
||||
const localePreference = localStorage.getItem('joplin-locale-preference');
|
||||
if (localePreference === 'en') return;
|
||||
|
||||
// Get user's preferred language from browser
|
||||
const browserLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
|
||||
|
||||
// Extract the base language code (e.g., 'fr' from 'fr-ca')
|
||||
const langCode = browserLang.split('-')[0];
|
||||
|
||||
// Check if we support this language
|
||||
if (!supportedLanguages.includes(langCode)) return;
|
||||
|
||||
window.location.href = getLocalePath(langCode) + '/';
|
||||
}
|
||||
|
||||
// Allow users to switch back to English and remember their preference
|
||||
function setLocalePreference(locale) {
|
||||
localStorage.setItem('joplin-locale-preference', locale);
|
||||
}
|
||||
|
||||
// Expose globally for language switcher links
|
||||
window.setLocalePreference = setLocalePreference;
|
||||
|
||||
$(function () {
|
||||
setupMobileMenu();
|
||||
setupLocaleRedirect();
|
||||
void setupDownloadPage();
|
||||
});
|
||||
|
||||
164
Assets/WebsiteAssets/locales/de_DE.po
Normal file
164
Assets/WebsiteAssets/locales/de_DE.po
Normal file
@@ -0,0 +1,164 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: de_DE\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:10
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:14
|
||||
msgid "/month"
|
||||
msgstr "/Monat"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:22
|
||||
msgid "/year"
|
||||
msgstr "/Jahr"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:71
|
||||
msgid "<a href=\"https://joplincloud.com\">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues."
|
||||
msgstr "<a href=\"https://joplincloud.com\">Joplin Cloud</a> ermöglicht es Ihnen, Ihre Notizen geräteübergreifend zu synchronisieren. Sie können Notizen veröffentlichen und mit Freunden, Familie oder Kollegen gemeinsam an Notizbüchern arbeiten."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:206
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow-lg\">Customise</span> it"
|
||||
msgstr "Passen Sie es <span class=\"frame-bg frame-bg-yellow-lg\">an</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:105
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow\">Multimedia</span> notes"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">Multimedia</span>-Notizen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
msgid "100% <span class=\"frame-bg frame-bg-yellow-lg\">your data</span>"
|
||||
msgstr "100 % <span class=\"frame-bg frame-bg-yellow-lg\">Ihre Daten</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:299
|
||||
msgid "A <span class=\"frame-bg frame-bg-yellow-lg\">French</span> Alternative"
|
||||
msgstr "Eine <span class=\"frame-bg frame-bg-yellow-lg\">französische</span> Alternative"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:237
|
||||
msgid "Access your notes from your computer, phone or tablet by synchronising with various services, including Joplin Cloud, Dropbox and OneDrive. The app is available on Windows, macOS, Linux, Android and iOS. A terminal app is also available!"
|
||||
msgstr "Greifen Sie von Ihrem Computer, Smartphone oder Tablet auf Ihre Notizen zu, indem Sie sie mit verschiedenen Diensten wie Joplin Cloud, Dropbox und OneDrive synchronisieren. Die App ist für Windows, macOS, Linux, Android und iOS verfügbar. Eine Terminal-App ist ebenfalls verfügbar!"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:121
|
||||
msgid "Already have a Joplin Cloud account? <a href=\"https://joplincloud.com\">Login now</a>"
|
||||
msgstr "Sie haben bereits ein Joplin-Cloud-Konto? <a href=\"https://joplincloud.com\">Jetzt anmelden</a>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:209
|
||||
msgid "Customise the app with plugins, custom themes and multiple text editors (Rich Text or Markdown). Or create your own scripts and plugins using the Extension API."
|
||||
msgstr "Passen Sie die App mit Plugins, eigenen Designs und verschiedenen Texteditoren (Rich Text oder Markdown) an. Oder erstellen Sie mit der Erweiterungs-API eigene Skripte und Plugins."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:243
|
||||
msgid "Download it now"
|
||||
msgstr "Jetzt herunterladen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:113
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:64
|
||||
msgid "Download the app"
|
||||
msgstr "App herunterladen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:214
|
||||
msgid "Find out more"
|
||||
msgstr "Mehr erfahren"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:55
|
||||
msgid "Free your <span class=\"frame-bg frame-bg-blue\">notes</span>"
|
||||
msgstr "Befreien Sie Ihre <span class=\"frame-bg frame-bg-blue\">Notizen</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:176
|
||||
msgid "Get the clipper"
|
||||
msgstr "Clipper herunterladen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:108
|
||||
msgid "Images, videos, PDFs and audio files are supported. Create math expressions and diagrams directly from the app. Take photos with the mobile app and save them to a note."
|
||||
msgstr "Bilder, Videos, PDFs und Audiodateien werden unterstützt. Erstellen Sie mathematische Ausdrücke und Diagramme direkt in der App. Machen Sie Fotos mit der mobilen App und speichern Sie sie in einer Notiz."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:328
|
||||
msgid "In the <span class=\"frame-bg frame-bg-yellow\">Press</span>"
|
||||
msgstr "In der <span class=\"frame-bg frame-bg-yellow\">Presse</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:68
|
||||
msgid "Joplin Cloud"
|
||||
msgstr "Joplin Cloud"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:302
|
||||
msgid "Joplin Cloud is based in France. This means your data is protected by strict European Union privacy laws. In addition, Joplin Cloud implements strong end-to-end encryption so that not even us can have access to your data."
|
||||
msgstr "Joplin Cloud hat seinen Sitz in Frankreich. Das bedeutet, dass Ihre Daten durch strenge Datenschutzgesetze der Europäischen Union geschützt sind. Darüber hinaus verwendet Joplin Cloud eine starke Ende-zu-Ende-Verschlüsselung, sodass nicht einmal wir Zugriff auf Ihre Daten haben."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:58
|
||||
msgid "Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device."
|
||||
msgstr "Joplin ist eine Open-Source-App für Notizen. Halten Sie Ihre Gedanken fest und greifen Sie sicher von jedem Gerät darauf zu."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:79
|
||||
msgid "Joplin Server Business"
|
||||
msgstr "Joplin Server Business"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:82
|
||||
msgid "Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business."
|
||||
msgstr "Joplin Server Business ist ein Synchronisationsserver, den Sie auf Ihrer eigenen Infrastruktur installieren können, sodass Ihre Daten innerhalb Ihres Unternehmens privat und sicher bleiben."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:263
|
||||
msgid "More about E2EE"
|
||||
msgstr "Mehr zu E2EE"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:392
|
||||
msgid "Our <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
msgstr "Unsere <span class=\"frame-bg frame-bg-blue-lg\">Sponsoren</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:46
|
||||
msgid "Our synchronisation and sharing <span class=\"frame-bg frame-bg-yellow\">solutions</span>"
|
||||
msgstr "Unsere <span class=\"frame-bg frame-bg-yellow\">Lösungen</span> für Synchronisation und Zusammenarbeit"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:91
|
||||
msgid "Pay Monthly"
|
||||
msgstr "Monatlich zahlen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:98
|
||||
msgid "Pay Yearly"
|
||||
msgstr "Jährlich zahlen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:168
|
||||
msgid "Save <span class=\"frame-bg frame-bg-blue\">web pages</span> <br>as notes"
|
||||
msgstr "Speichern Sie <span class=\"frame-bg frame-bg-blue\">Webseiten</span> <br>als Notizen"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:66
|
||||
msgid "Sign up with Joplin Cloud"
|
||||
msgstr "Mit Joplin Cloud registrieren"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
msgid "Synchronise and share your notes with our range of plans."
|
||||
msgstr "Synchronisieren und teilen Sie Ihre Notizen mit unseren verschiedenen Tarifen."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:395
|
||||
msgid "Thank you for your support!"
|
||||
msgstr "Vielen Dank für Ihre Unterstützung!"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:258
|
||||
msgid "The app is open source and your notes are saved to an open format, so you'll always have access to them. Uses End-To-End Encryption (E2EE) to secure your notes and ensure no-one but yourself can access them."
|
||||
msgstr "Die App ist Open Source und Ihre Notizen werden in einem offenen Format gespeichert, sodass Sie jederzeit Zugriff darauf haben. Sie verwendet Ende-zu-Ende-Verschlüsselung (E2EE), um Ihre Notizen zu schützen und sicherzustellen, dass niemand außer Ihnen darauf zugreifen kann."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:145
|
||||
msgid "Try it now"
|
||||
msgstr "Jetzt ausprobieren"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:171
|
||||
msgid "Use the web clipper extension, available on Chrome and Firefox, to save web pages or take screenshots as notes."
|
||||
msgstr "Verwenden Sie die Web-Clipper-Erweiterung für Chrome und Firefox, um Webseiten zu speichern oder Screenshots als Notizen zu erstellen."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:139
|
||||
msgid "With Joplin Cloud, share your notes with your friends, family or colleagues and collaborate on them."
|
||||
msgstr "Mit Joplin Cloud können Sie Ihre Notizen mit Freunden, Familie oder Kollegen teilen und gemeinsam daran arbeiten."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
msgid "Work <span class=\"frame-bg frame-bg-yellow\">together</span>"
|
||||
msgstr "Gemeinsam <span class=\"frame-bg frame-bg-yellow\">arbeiten</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:142
|
||||
msgid "You can also publish a note to the internet and share the URL with others."
|
||||
msgstr "Sie können eine Notiz auch im Internet veröffentlichen und die URL mit anderen teilen."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:234
|
||||
msgid "Your notes, <span class=\"frame-bg frame-bg-blue-lg\">everywhere</span> you are"
|
||||
msgstr "Ihre Notizen, <span class=\"frame-bg frame-bg-blue-lg\">überall</span>, wo Sie sind"
|
||||
@@ -1,8 +1,6 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -11,18 +9,235 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:63
|
||||
msgid "Download the app"
|
||||
msgstr "Télécharger l'application"
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:10
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:14
|
||||
msgid "/month"
|
||||
msgstr "/mois"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:54
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:22
|
||||
msgid "/year"
|
||||
msgstr "/an"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:71
|
||||
msgid ""
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> allows you to "
|
||||
"synchronise your notes across devices. It also lets you publish notes, and "
|
||||
"collaborate on notebooks with your friends, family or colleagues."
|
||||
msgstr ""
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> vous permet de "
|
||||
"synchroniser vos notes entre vos appareils. Il vous permet également de "
|
||||
"publier des notes et de collaborer sur des carnets avec vos amis, votre "
|
||||
"famille ou vos collègues."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:206
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow-lg\">Customise</span> it"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow-lg\">Personnalisez</span>-la"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:105
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow\">Multimedia</span> notes"
|
||||
msgstr "Notes <span class=\"frame-bg frame-bg-yellow\">multimédia</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
msgid "100% <span class=\"frame-bg frame-bg-yellow-lg\">your data</span>"
|
||||
msgstr "100 % <span class=\"frame-bg frame-bg-yellow-lg\">vos données</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:299
|
||||
msgid "A <span class=\"frame-bg frame-bg-yellow-lg\">French</span> Alternative"
|
||||
msgstr ""
|
||||
"Une alternative <span class=\"frame-bg frame-bg-yellow-lg\">française</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:237
|
||||
msgid ""
|
||||
"Access your notes from your computer, phone or tablet by synchronising with "
|
||||
"various services, including Joplin Cloud, Dropbox and OneDrive. The app is "
|
||||
"available on Windows, macOS, Linux, Android and iOS. A terminal app is also "
|
||||
"available!"
|
||||
msgstr ""
|
||||
"Accédez à vos notes depuis votre ordinateur, téléphone ou tablette en les "
|
||||
"synchronisant avec différents services, notamment Joplin Cloud, Dropbox et "
|
||||
"OneDrive. L’application est disponible sur Windows, macOS, Linux, Android et "
|
||||
"iOS. Une application en ligne de commande est également disponible !"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:121
|
||||
msgid ""
|
||||
"Already have a Joplin Cloud account? <a href=\"https://joplincloud.com"
|
||||
"\">Login now</a>"
|
||||
msgstr ""
|
||||
"Vous avez déjà un compte Joplin Cloud ? <a href=\"https://joplincloud.com"
|
||||
"\">Connectez-vous maintenant</a>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:209
|
||||
msgid ""
|
||||
"Customise the app with plugins, custom themes and multiple text editors "
|
||||
"(Rich Text or Markdown). Or create your own scripts and plugins using the "
|
||||
"Extension API."
|
||||
msgstr ""
|
||||
"Personnalisez l’application avec des extensions, des thèmes personnalisés et "
|
||||
"plusieurs éditeurs de texte (texte enrichi ou Markdown). Ou créez vos "
|
||||
"propres scripts et extensions grâce à l’API d’extension."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:243
|
||||
msgid "Download it now"
|
||||
msgstr "Téléchargez maintenant"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:113
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:64
|
||||
msgid "Download the app"
|
||||
msgstr "Télécharger l'appli"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:214
|
||||
msgid "Find out more"
|
||||
msgstr "En savoir plus"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:55
|
||||
msgid "Free your <span class=\"frame-bg frame-bg-blue\">notes</span>"
|
||||
msgstr "Libérez vos <span class=\"frame-bg frame-bg-blue\">notes</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:57
|
||||
msgid "Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device."
|
||||
msgstr "Joplin est une application libre de prise de notes. Capturez vos pensées et accédez-y de façon sécurisé depuis n'importe quel appareil."
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:176
|
||||
msgid "Get the clipper"
|
||||
msgstr "Obtenir le clipper"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:65
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:108
|
||||
msgid ""
|
||||
"Images, videos, PDFs and audio files are supported. Create math expressions "
|
||||
"and diagrams directly from the app. Take photos with the mobile app and save "
|
||||
"them to a note."
|
||||
msgstr ""
|
||||
"Les images, vidéos, PDF et fichiers audio sont pris en charge. Créez des "
|
||||
"expressions mathématiques et des diagrammes directement depuis "
|
||||
"l’application. Prenez des photos avec l’application mobile et enregistrez-"
|
||||
"les dans une note."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:328
|
||||
msgid "In the <span class=\"frame-bg frame-bg-yellow\">Press</span>"
|
||||
msgstr "Dans la <span class=\"frame-bg frame-bg-yellow\">presse</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:68
|
||||
msgid "Joplin Cloud"
|
||||
msgstr "Joplin Cloud"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:302
|
||||
msgid ""
|
||||
"Joplin Cloud is based in France. This means your data is protected by strict "
|
||||
"European Union privacy laws. In addition, Joplin Cloud implements strong end-"
|
||||
"to-end encryption so that not even us can have access to your data."
|
||||
msgstr ""
|
||||
"Joplin Cloud est basé en France. Cela signifie que vos données sont "
|
||||
"protégées par les lois strictes de l’Union européenne en matière de "
|
||||
"confidentialité. De plus, Joplin Cloud met en œuvre un chiffrement de bout "
|
||||
"en bout robuste afin que même nous ne puissions pas accéder à vos données."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:58
|
||||
msgid ""
|
||||
"Joplin is an open source note-taking app. Capture your thoughts and securely "
|
||||
"access them from any device."
|
||||
msgstr ""
|
||||
"Joplin est une application libre de prise de notes. Capturez vos pensées et "
|
||||
"accédez-y de façon sécurisée depuis n'importe quel appareil."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:79
|
||||
msgid "Joplin Server Business"
|
||||
msgstr "Joplin Server Business"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:82
|
||||
msgid ""
|
||||
"Joplin Server Business is a synchronisation server that you can install on "
|
||||
"your own infrastructure, so that your data remains private and secure within "
|
||||
"your business."
|
||||
msgstr ""
|
||||
"Joplin Server Business est un serveur de synchronisation que vous pouvez "
|
||||
"installer sur votre propre infrastructure, afin que vos données restent "
|
||||
"privées et sécurisées au sein de votre entreprise."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:263
|
||||
msgid "More about E2EE"
|
||||
msgstr "En savoir plus sur le chiffrement de bout en bout"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:392
|
||||
msgid "Our <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
msgstr "Nos <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:46
|
||||
msgid ""
|
||||
"Our synchronisation and sharing <span class=\"frame-bg frame-bg-yellow"
|
||||
"\">solutions</span>"
|
||||
msgstr ""
|
||||
"Nos <span class=\"frame-bg frame-bg-yellow\">solutions</span> de "
|
||||
"synchronisation et de partage"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:91
|
||||
msgid "Pay Monthly"
|
||||
msgstr "Payer mensuellement"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:98
|
||||
msgid "Pay Yearly"
|
||||
msgstr "Payer annuellement"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:168
|
||||
msgid ""
|
||||
"Save <span class=\"frame-bg frame-bg-blue\">web pages</span> <br>as notes"
|
||||
msgstr ""
|
||||
"Enregistrez des <span class=\"frame-bg frame-bg-blue\">pages web</span> "
|
||||
"<br>comme notes"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:66
|
||||
msgid "Sign up with Joplin Cloud"
|
||||
msgstr "S'inscrire sur Joplin Cloud"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
msgid "Synchronise and share your notes with our range of plans."
|
||||
msgstr "Synchronisez et partagez vos notes grâce à notre gamme d’offres."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:395
|
||||
msgid "Thank you for your support!"
|
||||
msgstr "Merci pour votre soutien !"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:258
|
||||
msgid ""
|
||||
"The app is open source and your notes are saved to an open format, so you'll "
|
||||
"always have access to them. Uses End-To-End Encryption (E2EE) to secure your "
|
||||
"notes and ensure no-one but yourself can access them."
|
||||
msgstr ""
|
||||
"L’application est open source et vos notes sont enregistrées dans un format "
|
||||
"ouvert, vous aurez donc toujours accès à celles-ci. Elle utilise le "
|
||||
"chiffrement de bout en bout (E2EE) pour sécuriser vos notes et garantir que "
|
||||
"personne d’autre que vous ne puisse y accéder."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:145
|
||||
msgid "Try it now"
|
||||
msgstr "Essayez-la maintenant"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:171
|
||||
msgid ""
|
||||
"Use the web clipper extension, available on Chrome and Firefox, to save web "
|
||||
"pages or take screenshots as notes."
|
||||
msgstr ""
|
||||
"Utilisez l’extension Web Clipper, disponible sur Chrome et Firefox, pour "
|
||||
"enregistrer des pages web ou des captures d’écran comme notes."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:139
|
||||
msgid ""
|
||||
"With Joplin Cloud, share your notes with your friends, family or colleagues "
|
||||
"and collaborate on them."
|
||||
msgstr ""
|
||||
"Avec Joplin Cloud, partagez vos notes avec vos amis, votre famille ou vos "
|
||||
"collègues et collaborez dessus."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
msgid "Work <span class=\"frame-bg frame-bg-yellow\">together</span>"
|
||||
msgstr "Travaillez <span class=\"frame-bg frame-bg-yellow\">ensemble</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:142
|
||||
msgid ""
|
||||
"You can also publish a note to the internet and share the URL with others."
|
||||
msgstr ""
|
||||
"Vous pouvez également publier une note sur Internet et partager son URL avec "
|
||||
"d’autres."
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:234
|
||||
msgid ""
|
||||
"Your notes, <span class=\"frame-bg frame-bg-blue-lg\">everywhere</span> you "
|
||||
"are"
|
||||
msgstr ""
|
||||
"Vos notes, <span class=\"frame-bg frame-bg-blue-lg\">partout</span> où vous "
|
||||
"êtes"
|
||||
|
||||
@@ -9,194 +9,213 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:13
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:9
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:10
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:14
|
||||
msgid "/month"
|
||||
msgstr "/月"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:19
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:22
|
||||
msgid "/year"
|
||||
msgstr "/年"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:8
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:71
|
||||
msgid ""
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> allows you to "
|
||||
"synchronise your notes across devices. It also lets you publish notes, and "
|
||||
"collaborate on notebooks with your friends, family or colleagues."
|
||||
msgstr ""
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> 允许您在不同的设备上同步"
|
||||
"您的笔记。它还可以让您发布笔记,并与您的朋友、家人或同事在笔记本上进行协作。"
|
||||
"<a href=\"https://joplincloud.com\">Joplin Cloud</a> 允许您在不同设备之间同步笔记。"
|
||||
"它还支持发布笔记,并与朋友、家人或同事协作共享笔记本。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:205
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:206
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow-lg\">Customise</span> it"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow-lg\">定制</span>它 根据您的需要"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow-lg\">自定义</span>它"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:104
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:105
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow\">Multimedia</span> notes"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">多媒体</span>说明"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">多媒体</span>笔记"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:256
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
msgid "100% <span class=\"frame-bg frame-bg-yellow-lg\">your data</span>"
|
||||
msgstr "百分之百<span class=\"frame-bg frame-bg-yellow-lg\">你的数据</span>"
|
||||
msgstr "100% <span class=\"frame-bg frame-bg-yellow-lg\">属于你的数据</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:298
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:299
|
||||
msgid "A <span class=\"frame-bg frame-bg-yellow-lg\">French</span> Alternative"
|
||||
msgstr "一个<span class=\"frame-bg frame-bg-yellow-lg\">法国</span>的替代方案"
|
||||
msgstr "一个<span class=\"frame-bg frame-bg-yellow-lg\">法国</span>替代方案"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:236
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:237
|
||||
msgid ""
|
||||
"Access your notes from your computer, phone or tablet by synchronising with "
|
||||
"various services, including Joplin Cloud, Dropbox and OneDrive. The app is "
|
||||
"available on Windows, macOS, Linux, Android and iOS. A terminal app is also "
|
||||
"available!"
|
||||
msgstr ""
|
||||
"通过与各种服务同步,包括Joplin Cloud、Dropbox和OneDrive,从你的电脑、手机或平"
|
||||
"板电脑访问你的笔记。该应用程序可在Windows、macOS、Linux、Android和iOS上使用。"
|
||||
"终端应用也可使用!"
|
||||
"通过与包括 Joplin Cloud、Dropbox 和 OneDrive 在内的多种服务同步,"
|
||||
"您可以在电脑、手机或平板上访问笔记。该应用支持 Windows、macOS、Linux、Android 和 iOS。"
|
||||
"同时还提供终端版本应用!"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:121
|
||||
msgid ""
|
||||
"Already have a Joplin Cloud account? <a href=\"https://joplincloud.com"
|
||||
"\">Login now</a>"
|
||||
"Already have a Joplin Cloud account? <a href=\"https://"
|
||||
"joplincloud.com\">Login now</a>"
|
||||
msgstr ""
|
||||
"已经拥有 Joplin Cloud 账户?<a href=\"https://joplincloud.com\">立即登录</a>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:208
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:209
|
||||
msgid ""
|
||||
"Customise the app with plugins, custom themes and multiple text editors "
|
||||
"(Rich Text or Markdown). Or create your own scripts and plugins using the "
|
||||
"Extension API."
|
||||
msgstr ""
|
||||
"用插件、自定义主题和多个文本编辑器(富文本或马克顿)来定制该应用程序。或者使"
|
||||
"用扩展API创建你自己的脚本和插件。"
|
||||
"通过插件、自定义主题和多种文本编辑器(富文本或 Markdown)来自定义应用。"
|
||||
"您也可以使用扩展 API 创建自己的脚本和插件。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:242
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:243
|
||||
msgid "Download it now"
|
||||
msgstr "下载该应用程序"
|
||||
msgstr "立即下载"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:112
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:63
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:113
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:64
|
||||
msgid "Download the app"
|
||||
msgstr "下载该应用程序"
|
||||
msgstr "下载应用"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:213
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:214
|
||||
msgid "Find out more"
|
||||
msgstr "了解更多"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:54
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:55
|
||||
msgid "Free your <span class=\"frame-bg frame-bg-blue\">notes</span>"
|
||||
msgstr "释放你的<span class=\"frame-bg frame-bg-blue\">笔记</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:175
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:176
|
||||
msgid "Get the clipper"
|
||||
msgstr "获取剪子"
|
||||
msgstr "获取网页剪藏器"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:107
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:108
|
||||
msgid ""
|
||||
"Images, videos, PDFs and audio files are supported. Create math expressions "
|
||||
"and diagrams directly from the app. Take photos with the mobile app and save "
|
||||
"them to a note."
|
||||
msgstr ""
|
||||
"Joplin,由于其起源和设计,适应并尊重中国的标准和规则。这保证了您的使用不受限"
|
||||
"制,以及您的使用数据的完全透明和安全。"
|
||||
"支持图片、视频、PDF 和音频文件。可在应用内直接创建数学公式和图表。"
|
||||
"还可通过移动端拍照并保存到笔记中。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:327
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:328
|
||||
msgid "In the <span class=\"frame-bg frame-bg-yellow\">Press</span>"
|
||||
msgstr ""
|
||||
msgstr "媒体<span class=\"frame-bg frame-bg-yellow\">报道</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:5
|
||||
msgid "Joplin Cloud <span class=\"frame-bg frame-bg-yellow\">plans</span>"
|
||||
msgstr "乔普林云<span class=\"frame-bg frame-bg-yellow\">计划</span>"
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:68
|
||||
msgid "Joplin Cloud"
|
||||
msgstr "Joplin Cloud"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:301
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:302
|
||||
msgid ""
|
||||
"Joplin Cloud is based in France. This means your data is protected by strict "
|
||||
"European Union privacy laws. In addition, Joplin Cloud implements strong end-"
|
||||
"to-end encryption so that not even us can have access to your data."
|
||||
msgstr ""
|
||||
"Joplin Cloud 位于法国。 这意味着您的数据受到严格的欧盟隐私法的保护。 此外,"
|
||||
"Joplin Cloud 实施了强大的端到端加密,因此即使是我们也无法访问您的数据。"
|
||||
"Joplin Cloud 位于法国,这意味着您的数据受到严格的欧盟隐私法规保护。"
|
||||
"此外,Joplin Cloud 采用强大的端到端加密技术,确保连我们也无法访问您的数据。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:57
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:58
|
||||
msgid ""
|
||||
"Joplin is an open source note-taking app. Capture your thoughts and securely "
|
||||
"access them from any device."
|
||||
msgstr ""
|
||||
"Joplin是一个开源的记事本应用程序。捕捉你的想法并从任何设备上安全地访问它们。"
|
||||
"Joplin 是一款开源笔记应用。随时记录想法,并可在任何设备上安全访问。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:262
|
||||
msgid "More about E2EE"
|
||||
msgstr "关于E2EE的更多信息"
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:79
|
||||
msgid "Joplin Server Business"
|
||||
msgstr "Joplin Server 商业版"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:391
|
||||
msgid "Our <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:82
|
||||
msgid ""
|
||||
"Joplin Server Business is a synchronisation server that you can install on "
|
||||
"your own infrastructure, so that your data remains private and secure within "
|
||||
"your business."
|
||||
msgstr ""
|
||||
"Joplin Server 商业版是一款可部署在您自有基础设施上的同步服务器,"
|
||||
"确保您的数据在企业内部保持私密与安全。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:23
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:263
|
||||
msgid "More about E2EE"
|
||||
msgstr "了解更多关于 E2EE"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:392
|
||||
msgid "Our <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
msgstr "我们的<span class=\"frame-bg frame-bg-blue-lg\">赞助商</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:46
|
||||
msgid ""
|
||||
"Our synchronisation and sharing <span class=\"frame-bg frame-bg-"
|
||||
"yellow\">solutions</span>"
|
||||
msgstr ""
|
||||
"我们的同步与共享<span class=\"frame-bg frame-bg-yellow\">解决方案</span>"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:91
|
||||
msgid "Pay Monthly"
|
||||
msgstr "月度"
|
||||
msgstr "按月付费"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:30
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:98
|
||||
msgid "Pay Yearly"
|
||||
msgstr "每年一次"
|
||||
msgstr "按年付费"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:167
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:168
|
||||
msgid ""
|
||||
"Save <span class=\"frame-bg frame-bg-blue\">web pages</span> <br>as notes"
|
||||
msgstr "保存<span class=\"frame-bg frame-bg-blue\">网页</span> <br>作为笔记"
|
||||
msgstr "将<span class=\"frame-bg frame-bg-blue\">网页</span><br>保存为笔记"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:65
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:66
|
||||
msgid "Sign up with Joplin Cloud"
|
||||
msgstr "与乔布林云签约"
|
||||
msgstr "注册 Joplin Cloud"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:394
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
msgid "Synchronise and share your notes with our range of plans."
|
||||
msgstr "通过我们的多种方案同步并共享您的笔记。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:395
|
||||
msgid "Thank you for your support!"
|
||||
msgstr ""
|
||||
msgstr "感谢您的支持!"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:258
|
||||
msgid ""
|
||||
"The app is open source and your notes are saved to an open format, so you'll "
|
||||
"always have access to them. Uses End-To-End Encryption (E2EE) to secure your "
|
||||
"notes and ensure no-one but yourself can access them."
|
||||
msgstr ""
|
||||
"该应用程序是开源的,你的笔记被保存为开放的格式,所以你将永远可以访问它们。使"
|
||||
"用端对端加密(E2EE)来保护你的笔记,确保除了你自己之外没有人可以访问它们。"
|
||||
"该应用为开源软件,笔记以开放格式保存,确保您始终可以访问。"
|
||||
"采用端到端加密(E2EE)保护您的笔记,确保只有您本人可以访问。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:144
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:145
|
||||
msgid "Try it now"
|
||||
msgstr "现在就试试吧"
|
||||
msgstr "立即体验"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:170
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:171
|
||||
msgid ""
|
||||
"Use the web clipper extension, available on Chrome and Firefox, to save web "
|
||||
"pages or take screenshots as notes."
|
||||
msgstr "使用Chrome和Firefox上的web clipper扩展,可以保存网页或截图作为笔记。"
|
||||
msgstr ""
|
||||
"使用适用于 Chrome 和 Firefox 的网页剪藏扩展,将网页或截图保存为笔记。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:139
|
||||
msgid ""
|
||||
"With Joplin Cloud, share your notes with your friends, family or colleagues "
|
||||
"and collaborate on them."
|
||||
msgstr "通过乔普林云,与你的朋友、家人或同事分享你的笔记,并进行合作。"
|
||||
msgstr ""
|
||||
"通过 Joplin Cloud,与朋友、家人或同事共享笔记并协作编辑。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:137
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
msgid "Work <span class=\"frame-bg frame-bg-yellow\">together</span>"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">一起</span>工作"
|
||||
msgstr "<span class=\"frame-bg frame-bg-yellow\">协作</span>工作"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:141
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:142
|
||||
msgid ""
|
||||
"You can also publish a note to the internet and share the URL with others."
|
||||
msgstr "您还可以将笔记发布到 Internet 并与其他人共享 URL。"
|
||||
msgstr "您还可以将笔记发布到互联网,并与他人分享链接。"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:233
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:234
|
||||
msgid ""
|
||||
"Your notes, <span class=\"frame-bg frame-bg-blue-lg\">everywhere</span> you "
|
||||
"are"
|
||||
msgstr ""
|
||||
"你的笔记<span class=\"frame-bg frame-bg-blue-lg\">你在哪里都可以</span>"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Joplin, due to its origin and design, adapts and respects Chinese "
|
||||
#~ "standards and rules. This guarantees your unrestricted use and complete "
|
||||
#~ "transparency and security of your usage data."
|
||||
#~ msgstr ""
|
||||
#~ "Joplin,由于其起源和设计,适应并尊重中国的标准和规则。这保证了您的使用不受"
|
||||
#~ "限制,以及您的使用数据的完全透明和安全。"
|
||||
"无论身在何处,您的笔记都<span class=\"frame-bg frame-bg-blue-lg\">随时可达</span>"
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Sun, 11 Jan 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.5]]></title><description><![CDATA[<h2>Improvements across desktop and mobile<a name="improvements-across-desktop-and-mobile" href="#improvements-across-desktop-and-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 10 Feb 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin will come preloaded on the HMD Terra M]]></title><description><![CDATA[<div style="overflow: auto;">
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260210-hmd-joplin-logo.png" width="200px" style="float: left; margin-right: 16px; margin-bottom: 16px;"/>
|
||||
<p>We’re happy to announce a collaboration with <a href="https://www.hmdsecure.com/">HMD Secure</a>, who will preload Joplin on their upcoming device, the HMD Terra M.</p>
|
||||
<p>This partnership brings Joplin to a new class of rugged, professional devices built for instant reliable communication, and reflects a shared focus on reliability, security, and long-term use.</p>
|
||||
</div>
|
||||
<h2>About HMD Secure<a name="about-hmd-secure" href="#about-hmd-secure" class="heading-anchor">🔗</a></h2>
|
||||
<p>HMD Secure Oy is a subsidiary of HMD (Human Mobile Devices), the largest European smartphone manufacturer. Headquartered in Finland, HMD Secure develops rugged, sovereign, and secure solutions for governments, defence, public safety, enterprise, and critical infrastructure.</p>
|
||||
<p>Built on a foundation of European R&D and enhanced supply chain traceability and security, HMD Secure offers organisations a trusted platform for sovereignty, resilience, and long-term control.</p>
|
||||
<h2>About the HMD Terra M<a name="about-the-hmd-terra-m" href="#about-the-hmd-terra-m" class="heading-anchor">🔗</a></h2>
|
||||
<p>The <a href="https://www.hmdsecure.com/hmd-terra-m">HMD Terra M</a> is a compact, ultra-rugged smart feature phone delivered as a fully managed, mission-critical communications solution. Designed for professionals operating in demanding environments, it combines MIL-STD-810H and IP68/IP69K durability with instant Push-to-Talk, programmable PTT and emergency keys, loud high-output audio, and long battery life.</p>
|
||||
<p>Built for rapid enterprise deployment, Terra M supports modern connectivity including dual SIM and eSIM, enterprise-grade MDM, and secure applications—enabling organisations to deploy, manage, and scale frontline communications reliably from day one.</p>
|
||||
<h2>Why Joplin on the Terra M<a name="why-joplin-on-the-terra-m" href="#why-joplin-on-the-terra-m" class="heading-anchor">🔗</a></h2>
|
||||
<p>Joplin’s <strong>offline-first design</strong>, <strong>end-to-end encryption</strong>, and focus on <strong>data ownership</strong> make it a natural fit for a device built to be trusted in the field. With Joplin preloaded, Terra M users can securely capture notes, procedures, and checklists from day one, even in challenging conditions.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260210-hmd-terra-m.jpg" alt="The HMD Terra M phone"></p>
|
||||
]]></description><link>https://joplinapp.org/news/20260210-hmd-terra-m</link><guid isPermaLink="false">20260210-hmd-terra-m</guid><pubDate>Tue, 10 Feb 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.5]]></title><description><![CDATA[<h2>Improvements across desktop and mobile<a name="improvements-across-desktop-and-mobile" href="#improvements-across-desktop-and-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<h3>More stable and consistent Markdown editing<a name="more-stable-and-consistent-markdown-editing" href="#more-stable-and-consistent-markdown-editing" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Markdown editor has been refined to feel more stable and closer to the final rendered view. Headings in the editor now more closely match how they appear when viewing a note, reducing the visual jump between editing and reading. Layout issues have also been addressed so elements like rendered checkboxes and images no longer cause the editor to shift unexpectedly while typing.</p>
|
||||
<p>The ABC music notation plugin appeared to be popular but had some limitations. With this new version, ABC is now part of the app, which means it can now work from published notes, and from the Rich Text editor!</p>
|
||||
@@ -505,23 +519,4 @@ sys 0m38.013s</p>
|
||||
<p>The extension is open source, with the code available here: <a href="https://github.com/laurent22/github-actions-logs-extension">https://github.com/laurent22/github-actions-logs-extension</a></p>
|
||||
<p>And to install it, follow this link:</p>
|
||||
<p><a href="https://chrome.google.com/webstore/detail/github-action-raw-log-vie/lgejlnoopmcdglhfjblaeldbcfnmjddf"><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20230116-extension-get-it-now.png" alt="Download GitHub Action Raw Log Viewer extension"></a></p>
|
||||
]]></description><link>https://joplinapp.org/news/20230116-github-actions-log-viewer</link><guid isPermaLink="false">20230116-github-actions-log-viewer</guid><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><twitter-text>Introducing the "GitHub Action Raw Log Viewer" extension for Chrome</twitter-text></item><item><title><![CDATA[Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)]]></title><description><![CDATA[<p>As was <a href="https://discourse.joplinapp.org/t/rfc-switch-to-agpl-license-for-joplin-server/16529">discussed last year</a>, Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0) for the desktop, mobile and CLI applications, as well as the web clipper.</p>
|
||||
<p>Any open source or commercial fork of Joplin will have to license any changes they make under AGPL, and share these changes back with the community. This is the main reason we switch to this license. It allows us to continue releasing the project as open source while ensuring that those who benefit commercially (or not) from it share back their changes.</p>
|
||||
<h2>What is the GPL license?<a name="what-is-the-gpl-license" href="#what-is-the-gpl-license" class="heading-anchor">🔗</a></h2>
|
||||
<p>The AGPL license is based on the GPL license. This is what tldr;Legal has to say about the GPL license:</p>
|
||||
<blockquote>
|
||||
<p>You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. (<a href="https://tldrlegal.com/license/gnu-general-public-license-v3-(gpl-3)">source</a>)</p>
|
||||
</blockquote>
|
||||
<h2>What is the AGPL license?<a name="what-is-the-agpl-license" href="#what-is-the-agpl-license" class="heading-anchor">🔗</a></h2>
|
||||
<p>This is the license we'll use for Joplin from now on:</p>
|
||||
<blockquote>
|
||||
<p>The AGPL license differs from the other GNU licenses in that it was built for network software. You can distribute modified versions if you keep track of the changes and the date you made them. As per usual with GNU licenses, you must license derivatives under AGPL. It provides the same restrictions and freedoms as the GPLv3 but with an additional clause which makes it so that source code must be distributed along with web publication. Since web sites and services are never distributed in the traditional sense, the AGPL is the GPL of the web. (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">source</a>)</p>
|
||||
</blockquote>
|
||||
<h2>What does it change for users?<a name="what-does-it-change-for-users" href="#what-does-it-change-for-users" class="heading-anchor">🔗</a></h2>
|
||||
<p>There is no changes for users of Joplin - the apps remain open sources and you can still use them freely.</p>
|
||||
<h2>What does it change for developers?<a name="what-does-it-change-for-developers" href="#what-does-it-change-for-developers" class="heading-anchor">🔗</a></h2>
|
||||
<p>Any code you develop for Joplin will also remain open source. The only difference is that we'll ask to sign an Individual Contributor License Agreement (CLA) to ensure that the copyright of the entire codebase remains with the Joplin organisation. This is necessary so that if we ever want to change the license again we are able to do so without having to get the agreement of each individual contributor afterwards (which would be nearly impossible).</p>
|
||||
<p>This is a bit of an extra constraint but it is hard to avoid. Contributor License Agreements are very common for GPL or AGPL projects. For example Apache, Canonical or Python all require their contributors to sign a CLA.</p>
|
||||
<h2>Questions?<a name="questions" href="#questions" class="heading-anchor">🔗</a></h2>
|
||||
<p>If you have any questions please let us know. Overall we believe this is a positive improvements for Joplin as it means any work derives from it will also benefit the project.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221221-agpl</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20230116-github-actions-log-viewer</link><guid isPermaLink="false">20230116-github-actions-log-viewer</guid><pubDate>Mon, 16 Jan 2023 00:00:00 GMT</pubDate><twitter-text>Introducing the "GitHub Action Raw Log Viewer" extension for Chrome</twitter-text></item></channel></rss>
|
||||
@@ -14,6 +14,7 @@
|
||||
<link rel="stylesheet" href="{{{assetUrls.css.fontawesome}}}">
|
||||
{{> openGraphTags}}
|
||||
{{> rssFeedLink}}
|
||||
{{> hreflangTags}}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"
|
||||
|
||||
@@ -26,6 +26,7 @@ https://github.com/laurent22/joplin/blob/dev/{{{sourceMarkdownFile}}}
|
||||
<meta name="theme-color" content="#000000" />
|
||||
{{> openGraphTags}}
|
||||
{{> rssFeedLink}}
|
||||
{{> hreflangTags}}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{cssBaseUrl}}/bootstrap5.0.2.min.css"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{{#availableLocales}}
|
||||
<link rel="alternate" hreflang="{{hreflang}}" href="https://joplinapp.org{{#pathPrefix}}/{{pathPrefix}}{{/pathPrefix}}{{currentPath}}" />
|
||||
{{/availableLocales}}
|
||||
<link rel="alternate" hreflang="x-default" href="https://joplinapp.org{{currentPath}}" />
|
||||
@@ -16,21 +16,17 @@
|
||||
<a href="{{baseUrl}}/news/" class="fw500">News</a>
|
||||
<a href="{{baseUrl}}/help/" class="fw500">Help</a>
|
||||
<a href="{{forumUrl}}" class="fw500">Forum</a>
|
||||
<!-- <a href="{{baseUrl}}/cn/" class="fw500">中文</a> -->
|
||||
|
||||
<!--
|
||||
<div class="dropdown language-switcher">
|
||||
<button class="fw500" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Language
|
||||
<button class="fw500" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-globe"></i> {{locale.code}}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{{#availableLocales}}
|
||||
<li><a class="dropdown-item {{#isActive}}active{{/isActive}}" href="{{baseUrl}}/{{pathPrefix}}" onclick="setLocalePreference('{{code}}')">{{name}}</a></li>
|
||||
{{/availableLocales}}
|
||||
</ul>
|
||||
</div>
|
||||
-->
|
||||
|
||||
|
||||
{{#showJoplinCloudLinks}}
|
||||
{{> joplinCloudButton}}
|
||||
@@ -39,7 +35,6 @@
|
||||
</div>
|
||||
<div class="col-9 text-right d-block d-md-none navbar-mobile-content">
|
||||
{{> twitterLink}}
|
||||
<!-- <a href="{{baseUrl}}/cn/" class="fw500 chinese-page-link">中文</a> -->
|
||||
{{> joplinCloudButton}}
|
||||
{{> supportButton}}
|
||||
|
||||
@@ -74,6 +69,13 @@
|
||||
{{/showJoplinCloudLinks}}
|
||||
{{> supportButton}}
|
||||
</div>
|
||||
|
||||
<div class="text-center menu-mobile-language">
|
||||
<p class="fw500 mobile-menu-language-label"><i class="fas fa-globe"></i> Language</p>
|
||||
{{#availableLocales}}
|
||||
<a href="{{baseUrl}}/{{pathPrefix}}" class="fw500 mobile-menu-link mobile-language-link {{#isActive}}active{{/isActive}}" onclick="setLocalePreference('{{code}}')">{{name}}</a>
|
||||
{{/availableLocales}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#showToc}}
|
||||
@@ -81,7 +83,7 @@
|
||||
{{/showToc}}
|
||||
|
||||
{{> socialFeeds}}
|
||||
|
||||
|
||||
<div>
|
||||
<p class="light-blue mobile-menu-link-bottom text-center">
|
||||
Copyright © 2016-{{yyyy}} Laurent Cozic
|
||||
|
||||
@@ -2,140 +2,156 @@ msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:13
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:9
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:10
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:14
|
||||
msgid "/month"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:19
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/partials/plan.mustache:22
|
||||
msgid "/year"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:8
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:71
|
||||
msgid "<a href=\"https://joplincloud.com\">Joplin Cloud</a> allows you to synchronise your notes across devices. It also lets you publish notes, and collaborate on notebooks with your friends, family or colleagues."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:205
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:206
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow-lg\">Customise</span> it"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:104
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:105
|
||||
msgid "<span class=\"frame-bg frame-bg-yellow\">Multimedia</span> notes"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:256
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
msgid "100% <span class=\"frame-bg frame-bg-yellow-lg\">your data</span>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:298
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:299
|
||||
msgid "A <span class=\"frame-bg frame-bg-yellow-lg\">French</span> Alternative"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:236
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:237
|
||||
msgid "Access your notes from your computer, phone or tablet by synchronising with various services, including Joplin Cloud, Dropbox and OneDrive. The app is available on Windows, macOS, Linux, Android and iOS. A terminal app is also available!"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:121
|
||||
msgid "Already have a Joplin Cloud account? <a href=\"https://joplincloud.com\">Login now</a>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:208
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:209
|
||||
msgid "Customise the app with plugins, custom themes and multiple text editors (Rich Text or Markdown). Or create your own scripts and plugins using the Extension API."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:242
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:243
|
||||
msgid "Download it now"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:112
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:63
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:113
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:64
|
||||
msgid "Download the app"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:213
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:214
|
||||
msgid "Find out more"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:54
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:55
|
||||
msgid "Free your <span class=\"frame-bg frame-bg-blue\">notes</span>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:175
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:176
|
||||
msgid "Get the clipper"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:107
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:108
|
||||
msgid "Images, videos, PDFs and audio files are supported. Create math expressions and diagrams directly from the app. Take photos with the mobile app and save them to a note."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:327
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:328
|
||||
msgid "In the <span class=\"frame-bg frame-bg-yellow\">Press</span>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:5
|
||||
msgid "Joplin Cloud <span class=\"frame-bg frame-bg-yellow\">plans</span>"
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:68
|
||||
msgid "Joplin Cloud"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:301
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:302
|
||||
msgid "Joplin Cloud is based in France. This means your data is protected by strict European Union privacy laws. In addition, Joplin Cloud implements strong end-to-end encryption so that not even us can have access to your data."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:57
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:58
|
||||
msgid "Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:262
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:79
|
||||
msgid "Joplin Server Business"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:82
|
||||
msgid "Joplin Server Business is a synchronisation server that you can install on your own infrastructure, so that your data remains private and secure within your business."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:263
|
||||
msgid "More about E2EE"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:391
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:392
|
||||
msgid "Our <span class=\"frame-bg frame-bg-blue-lg\">sponsors</span>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:23
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:46
|
||||
msgid "Our synchronisation and sharing <span class=\"frame-bg frame-bg-yellow\">solutions</span>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:91
|
||||
msgid "Pay Monthly"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:30
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:98
|
||||
msgid "Pay Yearly"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:167
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:168
|
||||
msgid "Save <span class=\"frame-bg frame-bg-blue\">web pages</span> <br>as notes"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:65
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:66
|
||||
msgid "Sign up with Joplin Cloud"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:394
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/plans.mustache:49
|
||||
msgid "Synchronise and share your notes with our range of plans."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:395
|
||||
msgid "Thank you for your support!"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:257
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:258
|
||||
msgid "The app is open source and your notes are saved to an open format, so you'll always have access to them. Uses End-To-End Encryption (E2EE) to secure your notes and ensure no-one but yourself can access them."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:144
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:145
|
||||
msgid "Try it now"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:170
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:171
|
||||
msgid "Use the web clipper extension, available on Chrome and Firefox, to save web pages or take screenshots as notes."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:139
|
||||
msgid "With Joplin Cloud, share your notes with your friends, family or colleagues and collaborate on them."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:137
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:138
|
||||
msgid "Work <span class=\"frame-bg frame-bg-yellow\">together</span>"
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:141
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:142
|
||||
msgid "You can also publish a note to the internet and share the URL with others."
|
||||
msgstr ""
|
||||
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:233
|
||||
#: /Users/laurent/src/joplin/Assets/WebsiteAssets/templates/front.mustache:234
|
||||
msgid "Your notes, <span class=\"frame-bg frame-bg-blue-lg\">everywhere</span> you are"
|
||||
msgstr ""
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"vips.dev": {
|
||||
"platforms": ["aarch64-darwin"],
|
||||
},
|
||||
"nodejs": "24.8.0",
|
||||
"nodejs": "24.9.0",
|
||||
"pkg-config": "latest",
|
||||
"python": "3.13.3",
|
||||
"bat": "latest",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"packageJsonLint": "node ./packages/tools/packageJsonLint.js",
|
||||
"syncFuzzer": "node ./packages/tools/fuzzer/sync-fuzzer.js",
|
||||
"syncFuzzer": "node ./packages/tools/fuzzer/cli.js",
|
||||
"postinstall": "husky && gulp build",
|
||||
"postPreReleasesToForum": "node ./packages/tools/postPreReleasesToForum",
|
||||
"publishAll": "git pull && yarn buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
@@ -86,7 +86,7 @@
|
||||
"gulp": "4.0.2",
|
||||
"husky": "9.1.7",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "16.2.6",
|
||||
"lint-staged": "16.2.7",
|
||||
"madge": "8.0.0",
|
||||
"npm-package-json-lint": "9.0.0",
|
||||
"typescript": "5.8.3"
|
||||
@@ -95,8 +95,8 @@
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "11.4.2",
|
||||
"nodemon": "3.1.10"
|
||||
"node-gyp": "11.5.0",
|
||||
"nodemon": "3.1.11"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"resolutions": {
|
||||
|
||||
@@ -402,7 +402,17 @@ async function fetchAllNotes() {
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_NOTE || model.type === BaseModel.TYPE_FOLDER) {
|
||||
if (model.type === BaseModel.TYPE_NOTE) {
|
||||
lines.push(`By default, the ${singular} will be moved **to the trash**. To permanently delete it, add the query parameter \`permanent=1\``);
|
||||
lines.push('');
|
||||
|
||||
lines.push('### DELETE /notes/:id/revisions');
|
||||
lines.push('');
|
||||
lines.push('Deletes all the revisions attached to this note.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (model.type === BaseModel.TYPE_FOLDER) {
|
||||
lines.push(`By default, the ${singular} will be moved **to the trash**. To permanently delete it, add the query parameter \`permanent=1\``);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
@@ -17,17 +17,11 @@ type Args = {
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public usage() {
|
||||
return 'publish [note]';
|
||||
return 'unpublish [note]';
|
||||
}
|
||||
|
||||
public description() {
|
||||
return _('Publishes a note to Joplin Server or Joplin Cloud');
|
||||
}
|
||||
|
||||
public options() {
|
||||
return [
|
||||
['-f, --force', _('Do not ask for user confirmation.')],
|
||||
];
|
||||
return _('Unpublishes a note from Joplin Server or Joplin Cloud');
|
||||
}
|
||||
|
||||
public enabled() {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"redux": "4.2.1",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.34.4",
|
||||
"sharp": "0.34.5",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
|
||||
@@ -99,6 +99,10 @@ describe('MdToHtml', () => {
|
||||
'',
|
||||
];
|
||||
|
||||
// Use this to generate the needed file:
|
||||
|
||||
// await writeFile('/path/to/actual.html', actualHtml, 'utf-8');
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(msg.join('\n'));
|
||||
|
||||
|
||||
10
packages/app-cli/tests/md_to_html/external_embed2.html
Normal file
10
packages/app-cli/tests/md_to_html/external_embed2.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<p>Link: <a data-from-md title='https://www.youtube.com/watch?v=iJqe9pC-z-Y' href='https://www.youtube.com/watch?v=iJqe9pC-z-Y' onclick='postMessage("https://www.youtube.com/watch?v=iJqe9pC-z-Y", { resourceId: "" }); return false;'>https://www.youtube.com/watch?v=iJqe9pC-z-Y</a></p>
|
||||
<p>
|
||||
<div class="joplin-editable">
|
||||
<span class="joplin-source" data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<div class="joplin-youtube-player-rendered">
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/iJqe9pC-z-Y" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<p>Test</p>
|
||||
5
packages/app-cli/tests/md_to_html/external_embed2.md
Normal file
5
packages/app-cli/tests/md_to_html/external_embed2.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Link: https://www.youtube.com/watch?v=iJqe9pC-z-Y
|
||||
|
||||
https://www.youtube.com/watch?v=iJqe9pC-z-Y
|
||||
|
||||
Test
|
||||
@@ -1,27 +1,22 @@
|
||||
# generator-joplin
|
||||
# Plugin development
|
||||
|
||||
Scaffolds out a new Joplin plugin
|
||||
This documentation describes how to create a plugin, and how to work with the plugin builder framework and API.
|
||||
|
||||
## Installation
|
||||
|
||||
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/)).
|
||||
|
||||
```bash
|
||||
npm install -g yo
|
||||
npm install -g yo@4.3.1
|
||||
npm install -g generator-joplin
|
||||
```
|
||||
|
||||
Then generate your new project:
|
||||
|
||||
```bash
|
||||
yo joplin
|
||||
yo --node-package-manager npm joplin
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To test the generator for development purposes, follow the instructions there: https://yeoman.io/authoring/#running-the-generator
|
||||
This is a template to create a new Joplin plugin.
|
||||
|
||||
## Structure
|
||||
|
||||
The main two files you will want to look at are:
|
||||
@@ -39,6 +34,10 @@ To build the plugin, simply run `npm run dist`.
|
||||
|
||||
The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript.
|
||||
|
||||
## Updating the manifest version number
|
||||
|
||||
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.
|
||||
|
||||
## Publishing the plugin
|
||||
|
||||
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:
|
||||
@@ -67,6 +66,13 @@ By default, the compiler (webpack) is going to compile `src/index.ts` only (as w
|
||||
|
||||
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.
|
||||
|
||||
## More information
|
||||
|
||||
- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
|
||||
- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api)
|
||||
- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/)
|
||||
- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq)
|
||||
|
||||
## License
|
||||
|
||||
MIT © Laurent Cozic
|
||||
|
||||
@@ -73,4 +73,8 @@ export default class Joplin {
|
||||
*/
|
||||
require(_path: string): any;
|
||||
versionInfo(): Promise<import("./types").VersionInfo>;
|
||||
/**
|
||||
* Tells whether the current theme is a dark one or not.
|
||||
*/
|
||||
shouldUseDarkColors(): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ClipboardContent } from './types';
|
||||
export default class JoplinClipboard {
|
||||
private electronClipboard_;
|
||||
private electronNativeImage_;
|
||||
@@ -26,4 +27,19 @@ export default class JoplinClipboard {
|
||||
* For example [ 'text/plain', 'text/html' ]
|
||||
*/
|
||||
availableFormats(): Promise<string[]>;
|
||||
/**
|
||||
* Writes multiple formats to the clipboard simultaneously.
|
||||
* This allows setting both text/plain and text/html at the same time.
|
||||
*
|
||||
* <span class="platform-desktop">desktop</span>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await joplin.clipboard.write({
|
||||
* text: 'Plain text version',
|
||||
* html: '<strong>HTML version</strong>'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
write(content: ClipboardContent): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import Plugin from '../Plugin';
|
||||
* now, are not well documented. You can find the list directly on GitHub
|
||||
* though at the following locations:
|
||||
*
|
||||
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
|
||||
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/WindowCommandsAndDialogs/commands)
|
||||
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
|
||||
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts)
|
||||
*
|
||||
@@ -25,8 +25,13 @@ import Plugin from '../Plugin';
|
||||
* commands can be found in these places:
|
||||
*
|
||||
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/commands)
|
||||
* * [Note screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-mobile/components/screens/Note/commands)
|
||||
* * [Editor commands](https://github.com/laurent22/joplin/blob/dev/packages/app-mobile/components/NoteEditor/commandDeclarations.ts)
|
||||
*
|
||||
* Additionally, certain global commands have the same implementation on both platforms:
|
||||
*
|
||||
* * [Shared global commands](https://github.com/laurent22/joplin/tree/dev/packages/lib/commands)
|
||||
*
|
||||
* ## Executing editor commands
|
||||
*
|
||||
* There might be a situation where you want to invoke editor commands
|
||||
|
||||
@@ -42,9 +42,11 @@ export default class JoplinSettings {
|
||||
*/
|
||||
values(keys: string[] | string): Promise<Record<string, unknown>>;
|
||||
/**
|
||||
* @deprecated Use joplin.settings.values()
|
||||
* Gets a setting value (only applies to setting you registered from your plugin).
|
||||
*
|
||||
* Gets a setting value (only applies to setting you registered from your plugin)
|
||||
* Note: If you want to retrieve all your plugin settings, for example when the plugin starts,
|
||||
* it is recommended to use the `values()` function instead - it will be much faster than
|
||||
* calling `value()` multiple times.
|
||||
*/
|
||||
value(key: string): Promise<any>;
|
||||
/**
|
||||
@@ -52,11 +54,15 @@ export default class JoplinSettings {
|
||||
*/
|
||||
setValue(key: string, value: any): Promise<void>;
|
||||
/**
|
||||
* Gets a global setting value, including app-specific settings and those set by other plugins.
|
||||
* Gets global setting values, including app-specific settings and those set by other plugins.
|
||||
*
|
||||
* The list of available settings is not documented yet, but can be found by looking at the source code:
|
||||
*
|
||||
* https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142
|
||||
* https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts
|
||||
*/
|
||||
globalValues(keys: string[]): Promise<any[]>;
|
||||
/**
|
||||
* @deprecated Use joplin.settings.globalValues()
|
||||
*/
|
||||
globalValue(key: string): Promise<any>;
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,17 @@ import JoplinViewsEditors from './JoplinViewsEditor';
|
||||
/**
|
||||
* This namespace provides access to view-related services.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
* ## Creating a view
|
||||
*
|
||||
* 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. 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.
|
||||
*
|
||||
* ## The `webviewApi` object
|
||||
*
|
||||
* Within a view, you can use the global object `webviewApi` for various utility functions, such as
|
||||
* sending messages or displaying context menu. Refer to [[WebviewApi]] for the full documentation.
|
||||
*/
|
||||
export default class JoplinViews {
|
||||
private store;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Plugin from '../Plugin';
|
||||
import { ButtonSpec, ViewHandle, DialogResult } from './types';
|
||||
import { ButtonSpec, ViewHandle, DialogResult, Toast } from './types';
|
||||
/**
|
||||
* Allows creating and managing dialogs. A dialog is modal window that
|
||||
* contains a webview and a row of buttons. You can update the
|
||||
@@ -43,6 +43,10 @@ export default class JoplinViewsDialogs {
|
||||
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
|
||||
*/
|
||||
showMessageBox(message: string): Promise<number>;
|
||||
/**
|
||||
* Displays a Toast notification in the corner of the application screen.
|
||||
*/
|
||||
showToast(toast: Toast): Promise<void>;
|
||||
/**
|
||||
* Displays a dialog to select a file or a directory. Same options and
|
||||
* output as
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import Plugin from '../Plugin';
|
||||
import { ActivationCheckCallback, ViewHandle, UpdateCallback } from './types';
|
||||
import { ActivationCheckCallback, ViewHandle, UpdateCallback, EditorPluginCallbacks } from './types';
|
||||
interface SaveNoteOptions {
|
||||
/**
|
||||
* The ID of the note to save. This should match either:
|
||||
* - The ID of the note currently being edited
|
||||
* - The ID of a note that was very recently open in the editor.
|
||||
*
|
||||
* This property is present to ensure that the note editor doesn't write
|
||||
* to the wrong note just after switching notes.
|
||||
*/
|
||||
noteId: string;
|
||||
/** The note's new content. */
|
||||
body: string;
|
||||
}
|
||||
/**
|
||||
* Allows creating alternative note editors. You can create a view to handle loading and saving the
|
||||
* note, and do your own rendering.
|
||||
@@ -41,10 +54,18 @@ export default class JoplinViewsEditors {
|
||||
private store;
|
||||
private plugin;
|
||||
private activationCheckHandlers_;
|
||||
private unhandledActivationCheck_;
|
||||
constructor(plugin: Plugin, store: any);
|
||||
private controller;
|
||||
/**
|
||||
* Registers a new editor plugin. Joplin will call the provided callback to create new editor views
|
||||
* associated with the plugin as necessary (e.g. when a new editor is created in a new window).
|
||||
*/
|
||||
register(viewId: string, callbacks: EditorPluginCallbacks): Promise<void>;
|
||||
/**
|
||||
* Creates a new editor view
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
create(id: string): Promise<ViewHandle>;
|
||||
/**
|
||||
@@ -60,14 +81,21 @@ export default class JoplinViewsEditors {
|
||||
*/
|
||||
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this for example when the current note
|
||||
* is changed, or when the application is opened. At that point should can check the current
|
||||
* note and decide whether your editor should be activated or not. If it should return `true`,
|
||||
* otherwise return `false`.
|
||||
* Saves the content of the editor, without calling `onUpdate` for editors in the same window.
|
||||
*/
|
||||
saveNote(handle: ViewHandle, props: SaveNoteOptions): Promise<void>;
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*
|
||||
* @deprecated - `onActivationCheck` should be provided when the editor is first created with
|
||||
* `editor.register`.
|
||||
*/
|
||||
onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise<void>;
|
||||
/**
|
||||
* Emitted when the editor content should be updated. This for example when the currently
|
||||
* Emitted when your editor content should be updated. This is for example when the currently
|
||||
* selected note changes, or when the user makes the editor visible.
|
||||
*/
|
||||
onUpdate(handle: ViewHandle, callback: UpdateCallback): Promise<void>;
|
||||
@@ -86,3 +114,4 @@ export default class JoplinViewsEditors {
|
||||
*/
|
||||
isVisible(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
export {};
|
||||
|
||||
@@ -80,5 +80,9 @@ export default class JoplinViewsPanels {
|
||||
* Tells whether the panel is visible or not
|
||||
*/
|
||||
visible(handle: ViewHandle): Promise<boolean>;
|
||||
/**
|
||||
* Assuming that the current panel is an editor plugin view, returns
|
||||
* whether the editor plugin view supports editing the current note.
|
||||
*/
|
||||
isActive(handle: ViewHandle): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ export default class JoplinWorkspace {
|
||||
filterEditorContextMenu(handler: FilterHandler<EditContextMenuFilterObject>): void;
|
||||
/**
|
||||
* Gets the currently selected note. Will be `null` if no note is selected.
|
||||
*
|
||||
* On desktop, this returns the selected note in the focused window.
|
||||
*/
|
||||
selectedNote(): Promise<any>;
|
||||
/**
|
||||
@@ -93,5 +95,12 @@ export default class JoplinWorkspace {
|
||||
* Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes.
|
||||
*/
|
||||
selectedNoteIds(): Promise<string[]>;
|
||||
/**
|
||||
* Gets the last hash (note section ID) from cross-note link targeting specific section.
|
||||
* New hash is available after `onNoteSelectionChange()` is triggered.
|
||||
* Example of cross-note link where `hello-world` is a hash: [Other Note Title](:/9bc9a5cb83f04554bf3fd3e41b4bb415#hello-world).
|
||||
* Method returns empty value when a note was navigated with method other than cross-note link containing valid hash.
|
||||
*/
|
||||
selectedNoteHash(): Promise<string>;
|
||||
}
|
||||
export {};
|
||||
|
||||
@@ -372,6 +372,19 @@ export interface DialogResult {
|
||||
formData?: any;
|
||||
}
|
||||
|
||||
export enum ToastType {
|
||||
Info = 'info',
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export interface Toast {
|
||||
message: string;
|
||||
type?: ToastType;
|
||||
duration?: number;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -384,9 +397,40 @@ export interface Rectangle {
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export type ActivationCheckCallback = ()=> Promise<boolean>;
|
||||
export interface EditorUpdateEvent {
|
||||
newBody: string;
|
||||
noteId: string;
|
||||
}
|
||||
export type UpdateCallback = (event: EditorUpdateEvent)=> Promise<void>;
|
||||
|
||||
export type UpdateCallback = ()=> Promise<void>;
|
||||
|
||||
export interface ActivationCheckEvent {
|
||||
handle: ViewHandle;
|
||||
noteId: string;
|
||||
}
|
||||
export type ActivationCheckCallback = (event: ActivationCheckEvent)=> Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Required callbacks for creating an editor plugin.
|
||||
*/
|
||||
export interface EditorPluginCallbacks {
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*/
|
||||
onActivationCheck: ActivationCheckCallback;
|
||||
|
||||
/**
|
||||
* Emitted when an editor view is created. This happens, for example, when a new window containing
|
||||
* a new editor is created.
|
||||
*
|
||||
* This callback should set the editor plugin's HTML using `editors.setHtml`, add scripts to the editor
|
||||
* with `editors.addScript`, and optionally listen for external changes using `editors.onUpdate`.
|
||||
*/
|
||||
onSetup: (handle: ViewHandle)=> Promise<void>;
|
||||
}
|
||||
|
||||
export type VisibleHandler = ()=> Promise<void>;
|
||||
|
||||
@@ -395,6 +439,8 @@ export interface EditContextMenuFilterObject {
|
||||
}
|
||||
|
||||
export interface EditorActivationCheckFilterObject {
|
||||
effectiveNoteId: string;
|
||||
windowId: string;
|
||||
activatedEditors: {
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
@@ -404,6 +450,20 @@ export interface EditorActivationCheckFilterObject {
|
||||
|
||||
export type FilterHandler<T> = (object: T)=> Promise<T>;
|
||||
|
||||
export type CommandArgument = string|number|object|boolean|null;
|
||||
|
||||
export interface MenuTemplateItem {
|
||||
label?: string;
|
||||
command?: string;
|
||||
commandArgs?: CommandArgument[];
|
||||
}
|
||||
|
||||
export interface WebviewApi {
|
||||
postMessage: (message: object)=> unknown;
|
||||
onMessage: (message: object)=> void;
|
||||
menuPopupFromTemplate: (template: MenuTemplateItem[])=> void;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Settings types
|
||||
// =================================================================
|
||||
@@ -528,6 +588,30 @@ export interface SettingSection {
|
||||
*/
|
||||
export type Path = string[];
|
||||
|
||||
// =================================================================
|
||||
// Clipboard API types
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Represents content that can be written to the clipboard in multiple formats.
|
||||
*/
|
||||
export interface ClipboardContent {
|
||||
/**
|
||||
* Plain text representation of the content
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* HTML representation of the content
|
||||
*/
|
||||
html?: string;
|
||||
|
||||
/**
|
||||
* Image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format
|
||||
*/
|
||||
image?: string;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Content Script types
|
||||
// =================================================================
|
||||
@@ -609,6 +693,27 @@ export interface CodeMirrorControl {
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
enableLanguageDataAutocomplete: { of: (enabled: boolean)=> any };
|
||||
|
||||
/**
|
||||
* A CodeMirror [facet](https://codemirror.net/docs/ref/#state.EditorState.facet) that contains
|
||||
* the ID of the note currently open in the editor.
|
||||
*
|
||||
* Access the value of this facet using
|
||||
* ```ts
|
||||
* const noteIdFacet = editorControl.joplinExtensions.noteIdFacet;
|
||||
* const editorState = editorControl.editor.state;
|
||||
* const noteId = editorState.facet(noteIdFacet);
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- No better type available
|
||||
noteIdFacet: any;
|
||||
/**
|
||||
* A CodeMirror [StateEffect](https://codemirror.net/docs/ref/#state.StateEffect) that is
|
||||
* included in a [Transaction](https://codemirror.net/docs/ref/#state.Transaction) when the
|
||||
* note ID changes.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- No better type available
|
||||
setNoteIdEffect: any;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,25 +3,26 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive",
|
||||
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
|
||||
"prepare": "npm run dist",
|
||||
"update": "npm install -g generator-joplin && yo joplin --update"
|
||||
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force",
|
||||
"updateVersion": "webpack --env joplin-plugin-config=updateVersion"
|
||||
},
|
||||
"keywords": [
|
||||
"joplin-plugin"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.0.14",
|
||||
"copy-webpack-plugin": "^6.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob": "^7.1.6",
|
||||
"@types/node": "^18.7.13",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"fs-extra": "^10.1.0",
|
||||
"glob": "^8.0.3",
|
||||
"on-build-webpack": "^0.1.0",
|
||||
"tar": "^6.0.5",
|
||||
"ts-loader": "^7.0.5",
|
||||
"typescript": "^3.9.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"tar": "^6.1.11",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^4.8.2",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"chalk": "^4.1.0",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import joplin from 'api';
|
||||
import { MenuItem } from 'api/types';
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
@@ -21,5 +22,29 @@ joplin.plugins.register({
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await joplin.workspace.filterEditorContextMenu(async (object: any) => {
|
||||
const newItems: MenuItem[] = [];
|
||||
|
||||
newItems.push({
|
||||
label: 'filterEditorContextMenu test 1',
|
||||
commandName: 'newNote',
|
||||
commandArgs: ['Created from context menu 1'],
|
||||
});
|
||||
|
||||
newItems.push({
|
||||
type: 'separator',
|
||||
});
|
||||
|
||||
newItems.push({
|
||||
label: 'filterEditorContextMenu test 2',
|
||||
commandName: 'newNote',
|
||||
commandArgs: ['Created from context menu 2'],
|
||||
});
|
||||
|
||||
object.items = object.items.concat(newItems);
|
||||
|
||||
return object;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
"target": "es2015",
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"baseUrl": ".",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
]
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,21 @@
|
||||
// update, you can easily restore the functionality you've added.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
const chalk = require('chalk');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
const WebpackOnBuildPlugin = require('on-build-webpack');
|
||||
const tar = require('tar');
|
||||
const glob = require('glob');
|
||||
const execSync = require('child_process').execSync;
|
||||
|
||||
// AUTO-GENERATED by updateCategories
|
||||
const allPossibleCategories = [{ 'name': 'appearance' }, { 'name': 'developer tools' }, { 'name': 'productivity' }, { 'name': 'themes' }, { 'name': 'integrations' }, { 'name': 'viewer' }, { 'name': 'search' }, { 'name': 'tags' }, { 'name': 'editor' }, { 'name': 'files' }, { 'name': 'personal knowledge management' }];
|
||||
// AUTO-GENERATED by updateCategories
|
||||
|
||||
const rootDir = path.resolve(__dirname);
|
||||
const userConfigFilename = './plugin.config.json';
|
||||
const userConfigPath = path.resolve(rootDir, userConfigFilename);
|
||||
@@ -23,19 +28,34 @@ const distDir = path.resolve(rootDir, 'dist');
|
||||
const srcDir = path.resolve(rootDir, 'src');
|
||||
const publishDir = path.resolve(rootDir, 'publish');
|
||||
|
||||
const userConfig = Object.assign({}, {
|
||||
const userConfig = {
|
||||
extraScripts: [],
|
||||
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
|
||||
...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}),
|
||||
};
|
||||
|
||||
const manifestPath = `${srcDir}/manifest.json`;
|
||||
const packageJsonPath = `${rootDir}/package.json`;
|
||||
const allPossibleCategories = ['appearance', 'developer tools', 'productivity', 'themes', 'integrations', 'viewer', 'search', 'tags', 'editor', 'files', 'personal knowledge management'];
|
||||
const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
const manifest = readManifest(manifestPath);
|
||||
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
|
||||
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
|
||||
|
||||
const { builtinModules } = require('node:module');
|
||||
|
||||
// Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in
|
||||
// node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules.
|
||||
// We don't need to polyfill because the plugins run in Electron's Node environment.
|
||||
const moduleFallback = {};
|
||||
for (const moduleName of builtinModules) {
|
||||
moduleFallback[moduleName] = false;
|
||||
}
|
||||
|
||||
const getPackageJson = () => {
|
||||
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
};
|
||||
|
||||
function validatePackageJson() {
|
||||
const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const content = getPackageJson();
|
||||
if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) {
|
||||
console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`));
|
||||
}
|
||||
@@ -71,21 +91,45 @@ function currentGitInfo() {
|
||||
function validateCategories(categories) {
|
||||
if (!categories) return null;
|
||||
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
|
||||
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
|
||||
categories.forEach(category => {
|
||||
if (!allPossibleCategories.includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid Categories are: \n${allPossibleCategories}\n`);
|
||||
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`);
|
||||
});
|
||||
}
|
||||
|
||||
function validateScreenshots(screenshots) {
|
||||
if (!screenshots) return null;
|
||||
for (const screenshot of screenshots) {
|
||||
if (!screenshot.src) throw new Error('You must specify a src for each screenshot');
|
||||
|
||||
// Avoid attempting to download and verify URL screenshots.
|
||||
if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const screenshotType = screenshot.src.split('.').pop();
|
||||
if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`);
|
||||
|
||||
const screenshotPath = path.resolve(rootDir, screenshot.src);
|
||||
|
||||
// Max file size is 1MB
|
||||
const fileMaxSize = 1024;
|
||||
const fileSize = fs.statSync(screenshotPath).size / 1024;
|
||||
if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`);
|
||||
}
|
||||
}
|
||||
|
||||
function readManifest(manifestPath) {
|
||||
const content = fs.readFileSync(manifestPath, 'utf8');
|
||||
const output = JSON.parse(content);
|
||||
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
|
||||
validateCategories(output.categories);
|
||||
validateScreenshots(output.screenshots);
|
||||
return output;
|
||||
}
|
||||
|
||||
function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
@@ -99,18 +143,22 @@ function createPluginArchive(sourceDir, destPath) {
|
||||
cwd: sourceDir,
|
||||
sync: true,
|
||||
},
|
||||
distFiles
|
||||
distFiles,
|
||||
);
|
||||
|
||||
console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`));
|
||||
}
|
||||
|
||||
const writeManifest = (manifestPath, content) => {
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(content, null, '\t'), 'utf8');
|
||||
};
|
||||
|
||||
function createPluginInfo(manifestPath, destPath, jplFilePath) {
|
||||
const contentText = fs.readFileSync(manifestPath, 'utf8');
|
||||
const content = JSON.parse(contentText);
|
||||
content._publish_hash = `sha256:${fileSha256(jplFilePath)}`;
|
||||
content._publish_commit = currentGitInfo();
|
||||
fs.writeFileSync(destPath, JSON.stringify(content, null, '\t'), 'utf8');
|
||||
writeManifest(destPath, content);
|
||||
}
|
||||
|
||||
function onBuildCompleted() {
|
||||
@@ -137,14 +185,15 @@ const baseConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
...userConfig.webpackOverrides,
|
||||
};
|
||||
|
||||
const pluginConfig = Object.assign({}, baseConfig, {
|
||||
entry: './src/index.ts',
|
||||
const pluginConfig = { ...baseConfig, entry: './src/index.ts',
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
},
|
||||
fallback: moduleFallback,
|
||||
// JSON files can also be required from scripts so we include this.
|
||||
// https://github.com/joplin/plugin-bibtex/pull/2
|
||||
extensions: ['.js', '.tsx', '.ts', '.json'],
|
||||
@@ -171,26 +220,63 @@ const pluginConfig = Object.assign({}, baseConfig, {
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
] };
|
||||
|
||||
const extraScriptConfig = Object.assign({}, baseConfig, {
|
||||
|
||||
// These libraries can be included with require(...) or
|
||||
// joplin.require(...) from content scripts.
|
||||
const externalContentScriptLibraries = [
|
||||
'@codemirror/view',
|
||||
'@codemirror/state',
|
||||
'@codemirror/search',
|
||||
'@codemirror/language',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/highlight',
|
||||
'@codemirror/lint',
|
||||
'@codemirror/lang-html',
|
||||
'@codemirror/lang-markdown',
|
||||
'@codemirror/language-data',
|
||||
'@lezer/common',
|
||||
'@lezer/markdown',
|
||||
'@lezer/highlight',
|
||||
];
|
||||
|
||||
const extraScriptExternals = {};
|
||||
for (const library of externalContentScriptLibraries) {
|
||||
extraScriptExternals[library] = { commonjs: library };
|
||||
}
|
||||
|
||||
const extraScriptConfig = {
|
||||
...baseConfig,
|
||||
resolve: {
|
||||
alias: {
|
||||
api: path.resolve(__dirname, 'api'),
|
||||
},
|
||||
fallback: moduleFallback,
|
||||
extensions: ['.js', '.tsx', '.ts', '.json'],
|
||||
},
|
||||
});
|
||||
|
||||
// We support requiring @codemirror/... libraries through require('@codemirror/...')
|
||||
externalsType: 'commonjs',
|
||||
externals: extraScriptExternals,
|
||||
};
|
||||
|
||||
const createArchiveConfig = {
|
||||
stats: 'errors-only',
|
||||
entry: './dist/index.js',
|
||||
resolve: {
|
||||
fallback: moduleFallback,
|
||||
},
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: publishDir,
|
||||
},
|
||||
plugins: [new WebpackOnBuildPlugin(onBuildCompleted)],
|
||||
plugins: [{
|
||||
apply(compiler) {
|
||||
compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted);
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
function resolveExtraScriptPath(name) {
|
||||
@@ -222,20 +308,43 @@ function buildExtraScriptConfigs(userConfig) {
|
||||
|
||||
for (const scriptName of userConfig.extraScripts) {
|
||||
const scriptPaths = resolveExtraScriptPath(scriptName);
|
||||
output.push(Object.assign({}, extraScriptConfig, {
|
||||
entry: scriptPaths.entry,
|
||||
output: scriptPaths.output,
|
||||
}));
|
||||
output.push({ ...extraScriptConfig, entry: scriptPaths.entry,
|
||||
output: scriptPaths.output });
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function main(processArgv) {
|
||||
const yargs = require('yargs/yargs');
|
||||
const argv = yargs(processArgv).argv;
|
||||
const increaseVersion = version => {
|
||||
try {
|
||||
const s = version.split('.');
|
||||
const d = Number(s[s.length - 1]) + 1;
|
||||
s[s.length - 1] = `${d}`;
|
||||
return s.join('.');
|
||||
} catch (error) {
|
||||
error.message = `Could not parse version number: ${version}: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const configName = argv['joplin-plugin-config'];
|
||||
const updateVersion = () => {
|
||||
const packageJson = getPackageJson();
|
||||
packageJson.version = increaseVersion(packageJson.version);
|
||||
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
||||
|
||||
const manifest = readManifest(manifestPath);
|
||||
manifest.version = increaseVersion(manifest.version);
|
||||
writeManifest(manifestPath, manifest);
|
||||
|
||||
if (packageJson.version !== manifest.version) {
|
||||
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.`));
|
||||
} else {
|
||||
console.info(packageJson.version);
|
||||
}
|
||||
};
|
||||
|
||||
function main(environ) {
|
||||
const configName = environ['joplin-plugin-config'];
|
||||
if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag');
|
||||
|
||||
// Webpack configurations run in parallel, while we need them to run in
|
||||
@@ -270,22 +379,30 @@ function main(processArgv) {
|
||||
fs.mkdirpSync(publishDir);
|
||||
}
|
||||
|
||||
if (configName === 'updateVersion') {
|
||||
updateVersion();
|
||||
return [];
|
||||
}
|
||||
|
||||
return configs[configName];
|
||||
}
|
||||
|
||||
let exportedConfigs = [];
|
||||
|
||||
try {
|
||||
exportedConfigs = main(process.argv);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
module.exports = (env) => {
|
||||
let exportedConfigs = [];
|
||||
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
try {
|
||||
exportedConfigs = main(env);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = exportedConfigs;
|
||||
if (!exportedConfigs.length) {
|
||||
// Nothing to do - for example where there are no external scripts to
|
||||
// compile.
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return exportedConfigs;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const fs = require('fs-extra');
|
||||
import { dialog, ipcMain } from 'electron';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain';
|
||||
import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import handleCustomProtocols, { CustomProtocolHandlers } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { clearTimeout, setTimeout } from 'timers';
|
||||
import { resolve } from 'path';
|
||||
import { defaultWindowId } from '@joplin/lib/reducer';
|
||||
@@ -68,7 +68,7 @@ export default class ElectronAppWrapper {
|
||||
|
||||
private initialCallbackUrl_: string = null;
|
||||
private updaterService_: AutoUpdaterService = null;
|
||||
private customProtocolHandler_: CustomProtocolHandler = null;
|
||||
private customProtocolHandlers_: CustomProtocolHandlers|null = null;
|
||||
private updatePollInterval_: ReturnType<typeof setTimeout>|null = null;
|
||||
|
||||
private profileLocker_: FileLocker|null = null;
|
||||
@@ -578,6 +578,17 @@ export default class ElectronAppWrapper {
|
||||
this.electronApp_.quit();
|
||||
}
|
||||
|
||||
public quitWithSyncCheck(
|
||||
dispatch: (action: { type: string; [key: string]: unknown })=> void,
|
||||
syncPending: boolean,
|
||||
) {
|
||||
if (syncPending) {
|
||||
dispatch({ type: 'QUIT_SYNC_DIALOG_OPEN' });
|
||||
} else {
|
||||
this.quit();
|
||||
}
|
||||
}
|
||||
|
||||
public exit(errorCode = 0) {
|
||||
this.onExit();
|
||||
this.electronApp_.exit(errorCode);
|
||||
@@ -816,8 +827,12 @@ export default class ElectronAppWrapper {
|
||||
}
|
||||
};
|
||||
|
||||
public getCustomProtocolHandler() {
|
||||
return this.customProtocolHandler_;
|
||||
public getContentProtocolHandler() {
|
||||
return this.customProtocolHandlers_.appContent;
|
||||
}
|
||||
|
||||
public getPluginProtocolHandler() {
|
||||
return this.customProtocolHandlers_.pluginContent;
|
||||
}
|
||||
|
||||
private async fixLinuxAccessibility_() {
|
||||
@@ -857,7 +872,7 @@ export default class ElectronAppWrapper {
|
||||
|
||||
await this.fixLinuxAccessibility_();
|
||||
|
||||
this.customProtocolHandler_ = handleCustomProtocols();
|
||||
this.customProtocolHandlers_ = handleCustomProtocols();
|
||||
this.createWindow();
|
||||
|
||||
this.electronApp_.on('before-quit', () => {
|
||||
|
||||
@@ -58,7 +58,7 @@ import OcrDriverTesseract from '@joplin/lib/services/ocr/drivers/OcrDriverTesser
|
||||
import OcrDriverTranscribe from '@joplin/lib/services/ocr/drivers/OcrDriverTranscribe';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import { PackageInfo } from '@joplin/lib/versionInfo';
|
||||
import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { CustomContentProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
|
||||
import { refreshFolders } from '@joplin/lib/folders-screen-utils';
|
||||
import initializeCommandService from './utils/initializeCommandService';
|
||||
import OcrDriverBase from '@joplin/lib/services/ocr/OcrDriverBase';
|
||||
@@ -82,7 +82,7 @@ class Application extends BaseApplication {
|
||||
private checkAllPluginStartedIID_: any = null;
|
||||
private initPluginServiceDone_ = false;
|
||||
private ocrService_: OcrService;
|
||||
private protocolHandler_: CustomProtocolHandler;
|
||||
private protocolHandler_: CustomContentProtocolHandler;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
@@ -130,7 +130,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'renderer.fileUrls' || action.type === 'SETTING_UPDATE_ALL') {
|
||||
bridge().electronApp().getCustomProtocolHandler().setMediaAccessEnabled(
|
||||
bridge().electronApp().getContentProtocolHandler().setMediaAccessEnabled(
|
||||
Setting.value('renderer.fileUrls'),
|
||||
);
|
||||
}
|
||||
@@ -212,7 +212,12 @@ class Application extends BaseApplication {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: _('Open %s', app.electronApp().name), click: () => { app.mainWindow().show(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: _('Quit'), click: () => { void app.quit(); } },
|
||||
{ label: _('Quit'), click: () => {
|
||||
app.quitWithSyncCheck(
|
||||
(action: { type: string; [key: string]: unknown }) => this.store().dispatch(action),
|
||||
this.store().getState().syncPending,
|
||||
);
|
||||
} },
|
||||
]);
|
||||
app.createTray(contextMenu);
|
||||
}
|
||||
@@ -477,7 +482,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
addTask('app/set up custom protocol handler', async () => {
|
||||
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
|
||||
this.protocolHandler_ = bridge().electronApp().getContentProtocolHandler();
|
||||
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
|
||||
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('resourceDir'));
|
||||
|
||||
@@ -441,11 +441,11 @@ export class Bridge {
|
||||
}
|
||||
|
||||
public get Menu() {
|
||||
return require('electron').Menu;
|
||||
return Menu;
|
||||
}
|
||||
|
||||
public get MenuItem() {
|
||||
return require('electron').MenuItem;
|
||||
return MenuItem;
|
||||
}
|
||||
|
||||
public async openExternal(url: string) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useCallback, useState, useRef, useEffect, useId } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import StyledInput from '../style/StyledInput';
|
||||
import { IconSelector, ChangeEvent } from './IconSelector';
|
||||
@@ -145,6 +145,7 @@ export default function(props: Props) {
|
||||
title={_('Select emoji...')}
|
||||
icon={folderIcon}
|
||||
onChange={onFolderIconChange}
|
||||
themeId={props.themeId}
|
||||
/>
|
||||
<Button ml={1} title={_('Select file...')} onClick={onBrowseClick}/>
|
||||
{ folderIcon && <Button ml={1} title={_('Clear')} onClick={onClearClick}/> }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { EmojiButton } from '@joeattardi/emoji-button';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { loadScript } from '../utils/loadScript';
|
||||
import Button from '../Button/Button';
|
||||
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
import bridge from '../../services/bridge';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
export interface ChangeEvent {
|
||||
value: FolderIcon;
|
||||
@@ -16,12 +17,16 @@ interface Props {
|
||||
onChange: ChangeHandler;
|
||||
icon: FolderIcon | null;
|
||||
title: string;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
export const IconSelector = (props: Props) => {
|
||||
const [emojiButtonClassReady, setEmojiButtonClassReady] = useState<boolean>(false);
|
||||
const [picker, setPicker] = useState<EmojiButton>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const pickerTheme = useMemo(() => {
|
||||
return themeStyle(props.themeId).appearance;
|
||||
}, [props.themeId]);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const loadScripts = async () => {
|
||||
@@ -62,6 +67,7 @@ export const IconSelector = (props: Props) => {
|
||||
const p: EmojiButton = new (window as any).EmojiButton({
|
||||
zIndex: 10000,
|
||||
rootElement: buttonRef.current?.parentElement,
|
||||
theme: pickerTheme,
|
||||
});
|
||||
|
||||
const onEmoji = (selection: FolderIcon) => {
|
||||
@@ -76,7 +82,7 @@ export const IconSelector = (props: Props) => {
|
||||
p.off('emoji', onEmoji);
|
||||
p.destroyPicker();
|
||||
};
|
||||
}, [emojiButtonClassReady, props.onChange]);
|
||||
}, [emojiButtonClassReady, props.onChange, pickerTheme]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
picker.togglePicker(buttonRef.current);
|
||||
|
||||
@@ -44,6 +44,7 @@ import NoteEditor from './NoteEditor/NoteEditor';
|
||||
import PluginNotification from './PluginNotification/PluginNotification';
|
||||
import { Toast } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import QuitSyncDialog from './QuitSyncDialog';
|
||||
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
@@ -809,6 +810,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
themeId={this.props.themeId}
|
||||
toast={this.props.toast}
|
||||
/>
|
||||
<QuitSyncDialog themeId={this.props.themeId} />
|
||||
{messageComp}
|
||||
{layoutComp}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import { getMasterPasswordStatus, getMasterPasswordStatusMessage, checkHasMasterPasswordEncryptedData, masterPasswordIsValid, MasterPasswordStatus, resetMasterPassword, updateMasterPassword, getMasterPassword } from '@joplin/lib/services/e2ee/utils';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
@@ -184,6 +184,7 @@ interface Props {
|
||||
windowId: string;
|
||||
secondaryWindowFocused: boolean;
|
||||
showMenuBar: boolean;
|
||||
syncPending: boolean;
|
||||
}
|
||||
|
||||
const commandNames: string[] = menuCommandNames();
|
||||
@@ -362,7 +363,12 @@ function useMenu(props: Props) {
|
||||
const quitMenuItem = {
|
||||
label: _('Quit'),
|
||||
accelerator: keymapService.getAccelerator('quit'),
|
||||
click: () => { void bridge().electronApp().quit(); },
|
||||
click: () => {
|
||||
bridge().electronApp().quitWithSyncCheck(
|
||||
(action: { type: string; [key: string]: unknown }) => props.dispatch(action),
|
||||
props.syncPending,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const sortNoteFolderItems = (type: string) => {
|
||||
@@ -615,6 +621,18 @@ function useMenu(props: Props) {
|
||||
|
||||
...(shim.isMac() ? [] : profilesAndAppInstancesItems),
|
||||
|
||||
shim.isMac() ? noItem : {
|
||||
type: 'separator',
|
||||
},
|
||||
|
||||
shim.isMac() ? noItem : {
|
||||
label: _('Close Window'),
|
||||
accelerator: keymapService.getAccelerator('closeWindow'),
|
||||
click: () => {
|
||||
bridge().activeWindow()?.close();
|
||||
},
|
||||
},
|
||||
|
||||
shim.isMac() ? {
|
||||
label: _('Hide %s', 'Joplin'),
|
||||
platforms: ['darwin'],
|
||||
@@ -1027,6 +1045,7 @@ function useMenu(props: Props) {
|
||||
props.profileConfig,
|
||||
switchProfileMenuItems,
|
||||
menuItemDic,
|
||||
props.syncPending,
|
||||
]);
|
||||
|
||||
useMenuStates(menu, props);
|
||||
@@ -1113,6 +1132,7 @@ const mapStateToProps = (state: AppState): Partial<Props> => {
|
||||
noteListRendererIds: state.noteListRendererIds,
|
||||
noteListRendererId: state.settings['notes.listRendererId'],
|
||||
showMenuBar: state.settings.showMenuBar,
|
||||
syncPending: state.syncPending,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -56,17 +56,32 @@ const useWindowRefocusManager = (route: AppStateRoute) => {
|
||||
};
|
||||
|
||||
const useContainerSize = (container: HTMLElement|null) => {
|
||||
const [size, setSize] = useState({ width: container?.clientWidth ?? 0, height: container?.clientHeight ?? 0 });
|
||||
const [size, setSize] = useState({
|
||||
// Show the container as soon as possible: Default to the window size,
|
||||
// which is usually correct:
|
||||
width: container?.clientWidth ?? window.innerWidth,
|
||||
height: container?.clientHeight ?? window.innerHeight,
|
||||
});
|
||||
|
||||
const currentSizeRef = useRef(size);
|
||||
currentSizeRef.current = size;
|
||||
|
||||
useEffect(() => {
|
||||
if (!container) return () => {};
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
setSize({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight,
|
||||
});
|
||||
});
|
||||
const updateSizeIfDifferent = () => {
|
||||
const { width: lastWidth, height: lastHeight } = currentSizeRef.current;
|
||||
if (lastWidth !== container.clientWidth || lastHeight !== container.clientHeight) {
|
||||
setSize({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
// Ensure that the initial size is set, even if the ResizeObserver doesn't run the callback initially
|
||||
updateSizeIfDifferent();
|
||||
|
||||
const observer = new ResizeObserver(updateSizeIfDifferent);
|
||||
observer.observe(container);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
|
||||
@@ -5,7 +5,7 @@ import DialogButtonRow from './DialogButtonRow';
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const Countable = require('@joplin/lib/countable/Countable');
|
||||
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
|
||||
import Dialog from './Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
|
||||
interface NoteContentPropertiesDialogProps {
|
||||
themeId: number;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { getResourceIdFromMarkup } from './useContextMenu';
|
||||
|
||||
describe('useContextMenu', () => {
|
||||
const resourceId = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4';
|
||||
const resourceId2 = 'b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5';
|
||||
|
||||
it('should return resource ID when cursor is inside markdown image', () => {
|
||||
const line = ``;
|
||||
expect(getResourceIdFromMarkup(line, 0)).toBe(resourceId);
|
||||
expect(getResourceIdFromMarkup(line, 15)).toBe(resourceId);
|
||||
expect(getResourceIdFromMarkup(line, line.length - 1)).toBe(resourceId);
|
||||
});
|
||||
|
||||
it('should return null when cursor is outside markdown image', () => {
|
||||
const line = `Some text  more text`;
|
||||
expect(getResourceIdFromMarkup(line, 5)).toBeNull();
|
||||
expect(getResourceIdFromMarkup(line, line.length - 5)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle markdown image without alt text', () => {
|
||||
const line = ``;
|
||||
expect(getResourceIdFromMarkup(line, 5)).toBe(resourceId);
|
||||
});
|
||||
|
||||
it('should return resource ID when cursor is inside HTML img tag', () => {
|
||||
const line = `<img src=":/${resourceId}" />`;
|
||||
expect(getResourceIdFromMarkup(line, 10)).toBe(resourceId);
|
||||
});
|
||||
|
||||
it('should handle HTML img tag with additional attributes', () => {
|
||||
const line = `<img alt="test" src=":/${resourceId}" width="100" />`;
|
||||
expect(getResourceIdFromMarkup(line, 25)).toBe(resourceId);
|
||||
});
|
||||
|
||||
it('should return null when cursor is outside HTML img tag', () => {
|
||||
const line = `text <img src=":/${resourceId}" /> more`;
|
||||
expect(getResourceIdFromMarkup(line, 2)).toBeNull();
|
||||
expect(getResourceIdFromMarkup(line, line.length - 2)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return correct resource ID when multiple images on same line', () => {
|
||||
const line = ` `;
|
||||
expect(getResourceIdFromMarkup(line, 10)).toBe(resourceId);
|
||||
expect(getResourceIdFromMarkup(line, 50)).toBe(resourceId2);
|
||||
});
|
||||
|
||||
it('should return null for empty line', () => {
|
||||
expect(getResourceIdFromMarkup('', 0)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for line without images', () => {
|
||||
expect(getResourceIdFromMarkup('Just some regular text', 10)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-resource links', () => {
|
||||
const line = '';
|
||||
expect(getResourceIdFromMarkup(line, 10)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle cursor at exact boundaries of image markup', () => {
|
||||
const line = ``;
|
||||
expect(getResourceIdFromMarkup(line, 0)).toBe(resourceId);
|
||||
expect(getResourceIdFromMarkup(line, line.length)).toBe(resourceId);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,73 @@
|
||||
|
||||
import { ContextMenuParams, Event } from 'electron';
|
||||
import { useEffect, RefObject, useContext } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { EditContextMenuFilterObject, MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { ContextMenuItemType, ContextMenuOptions, buildMenuItems, handleEditorContextMenuFilter } from '../../../utils/contextMenuUtils';
|
||||
import { menuItems } from '../../../utils/contextMenu';
|
||||
import isItemId from '@joplin/lib/models/utils/isItemId';
|
||||
import { extractResourceUrls } from '@joplin/lib/urlUtils';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
|
||||
// Extract resource ID from image markup at a given cursor position within a line.
|
||||
// Returns the resource ID if the cursor is within an image markup, null otherwise.
|
||||
export const getResourceIdFromMarkup = (lineContent: string, cursorPosInLine: number): string | null => {
|
||||
const resourceUrls = extractResourceUrls(lineContent);
|
||||
if (!resourceUrls.length) return null;
|
||||
|
||||
for (const resourceInfo of resourceUrls) {
|
||||
const resourcePattern = new RegExp(`[:](/?${resourceInfo.itemId})`, 'g');
|
||||
let match;
|
||||
while ((match = resourcePattern.exec(lineContent)) !== null) {
|
||||
// Look backwards for ![ or <img
|
||||
let markupStart = lineContent.lastIndexOf('![', match.index);
|
||||
const imgTagStart = lineContent.lastIndexOf('<img', match.index);
|
||||
if (imgTagStart > markupStart) markupStart = imgTagStart;
|
||||
|
||||
if (markupStart === -1) continue;
|
||||
|
||||
// Find the end of the markup
|
||||
let markupEnd: number;
|
||||
if (lineContent[markupStart] === '!') {
|
||||
markupEnd = lineContent.indexOf(')', match.index);
|
||||
if (markupEnd !== -1) markupEnd += 1;
|
||||
} else {
|
||||
markupEnd = lineContent.indexOf('>', match.index);
|
||||
if (markupEnd !== -1) markupEnd += 1;
|
||||
}
|
||||
|
||||
if (markupEnd !== -1 && cursorPosInLine >= markupStart && cursorPosInLine <= markupEnd) {
|
||||
return resourceInfo.itemId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const menuUtils = new MenuUtils(CommandService.instance());
|
||||
|
||||
const imageClassName = 'cm-md-image';
|
||||
|
||||
// Shared helper to extract resource ID from a path/URL
|
||||
const pathToId = (path: string) => {
|
||||
const id = Resource.pathToId(path);
|
||||
return isItemId(id) ? id : '';
|
||||
};
|
||||
|
||||
interface ContextMenuProps {
|
||||
plugins: PluginStates;
|
||||
dispatch: Dispatch;
|
||||
editorCutText: ()=> void;
|
||||
editorCopyText: ()=> void;
|
||||
editorPaste: ()=> void;
|
||||
@@ -51,7 +99,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
return screenXY / zoomFraction;
|
||||
};
|
||||
|
||||
function pointerInsideEditor(params: ContextMenuParams) {
|
||||
const pointerInsideEditor = (params: ContextMenuParams, allowNonEditable = false) => {
|
||||
const x = params.x, y = params.y, isEditable = params.isEditable;
|
||||
const containerDoc = props.containerRef.current?.ownerDocument;
|
||||
const elements = containerDoc?.getElementsByClassName(props.editorClassName);
|
||||
@@ -59,7 +107,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// Note: We can't check inputFieldType here. When spellcheck is enabled,
|
||||
// params.inputFieldType is "none". When spellcheck is disabled,
|
||||
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
|
||||
if (!elements?.length || !isEditable) return false;
|
||||
if (!elements?.length || (!isEditable && !allowNonEditable)) return false;
|
||||
|
||||
// Checks whether the element the pointer clicked on is inside the editor.
|
||||
// This logic will need to be changed if the editor is eventually wrapped
|
||||
@@ -70,9 +118,109 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
const yScreen = convertFromScreenCoordinates(zoom, y);
|
||||
const intersectingElement = containerDoc.elementFromPoint(xScreen, yScreen);
|
||||
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
|
||||
}
|
||||
};
|
||||
|
||||
async function onContextMenu(event: Event, params: ContextMenuParams) {
|
||||
const getClickedImageContainer = (params: ContextMenuParams) => {
|
||||
const containerDoc = props.containerRef.current?.ownerDocument;
|
||||
if (!containerDoc) return null;
|
||||
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const xScreen = convertFromScreenCoordinates(zoom, params.x);
|
||||
const yScreen = convertFromScreenCoordinates(zoom, params.y);
|
||||
const clickedElement = containerDoc.elementFromPoint(xScreen, yScreen);
|
||||
|
||||
return clickedElement?.closest(`.${imageClassName}`) as HTMLElement | null;
|
||||
};
|
||||
|
||||
// Get resource ID from image markup at click position (not cursor position)
|
||||
const getResourceIdAtClickPos = (params: ContextMenuParams): string | null => {
|
||||
if (!editorRef.current) return null;
|
||||
|
||||
const editor = editorRef.current.editor;
|
||||
if (!editor) return null;
|
||||
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const x = convertFromScreenCoordinates(zoom, params.x);
|
||||
const y = convertFromScreenCoordinates(zoom, params.y);
|
||||
|
||||
const clickPos = editor.posAtCoords({ x, y });
|
||||
if (clickPos === null) return null;
|
||||
|
||||
const line = editor.state.doc.lineAt(clickPos);
|
||||
return getResourceIdFromMarkup(line.text, clickPos - line.from);
|
||||
};
|
||||
|
||||
const targetWindow = bridge().windowById(windowId);
|
||||
|
||||
const showImageContextMenu = async (resourceId: string) => {
|
||||
const menu = new Menu();
|
||||
const contextMenuOptions: ContextMenuOptions = {
|
||||
itemType: ContextMenuItemType.Image,
|
||||
resourceId,
|
||||
filename: null,
|
||||
mime: null,
|
||||
linkToCopy: null,
|
||||
linkToOpen: null,
|
||||
textToCopy: null,
|
||||
htmlToCopy: null,
|
||||
insertContent: () => {},
|
||||
isReadOnly: true,
|
||||
fireEditorEvent: () => {},
|
||||
htmlToMd: null,
|
||||
mdToHtml: null,
|
||||
};
|
||||
|
||||
const imageMenuItems = await buildMenuItems(menuItems(props.dispatch), contextMenuOptions);
|
||||
for (const item of imageMenuItems) {
|
||||
menu.append(item);
|
||||
}
|
||||
|
||||
menu.popup({ window: targetWindow });
|
||||
};
|
||||
|
||||
// Move the cursor to the line containing the image markup for a rendered image.
|
||||
// This ensures plugins that inspect cursor position (e.g. rich markdown, image resize)
|
||||
// show the correct context menu options.
|
||||
const moveCursorToImageLine = (imageContainer: HTMLElement) => {
|
||||
const editor = editorRef.current?.editor;
|
||||
if (!editor) return;
|
||||
|
||||
// The image widget stores its source document position as a data attribute.
|
||||
const sourceFrom = imageContainer.dataset.sourceFrom;
|
||||
if (sourceFrom === undefined) return;
|
||||
|
||||
const pos = Math.min(Number(sourceFrom), editor.state.doc.length);
|
||||
const line = editor.state.doc.lineAt(pos);
|
||||
editor.dispatch({
|
||||
selection: { anchor: line.from },
|
||||
});
|
||||
};
|
||||
|
||||
const onContextMenu = async (event: Event, params: ContextMenuParams) => {
|
||||
// Check if right-clicking on a rendered image first (images may not be "editable")
|
||||
const imageContainer = getClickedImageContainer(params);
|
||||
if (imageContainer && pointerInsideEditor(params, true)) {
|
||||
const imgElement = imageContainer.querySelector('img');
|
||||
if (imgElement) {
|
||||
const resourceId = pathToId(imgElement.src);
|
||||
if (resourceId) {
|
||||
event.preventDefault();
|
||||
moveCursorToImageLine(imageContainer);
|
||||
await showImageContextMenu(resourceId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if right-clicking on image markup text
|
||||
const markupResourceId = getResourceIdAtClickPos(params);
|
||||
if (markupResourceId && pointerInsideEditor(params)) {
|
||||
event.preventDefault();
|
||||
await showImageContextMenu(markupResourceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// For text context menu, require editable
|
||||
if (!pointerInsideEditor(params)) return;
|
||||
|
||||
// Don't show the default menu.
|
||||
@@ -131,34 +279,30 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
(editorRef.current as any).alignSelection(params);
|
||||
}
|
||||
|
||||
let filterObject: EditContextMenuFilterObject = {
|
||||
items: [],
|
||||
};
|
||||
const extraItems = await handleEditorContextMenuFilter({
|
||||
itemType: ContextMenuItemType.Text,
|
||||
});
|
||||
|
||||
filterObject = await eventManager.filterEmit('editorContextMenu', filterObject);
|
||||
|
||||
for (const item of filterObject.items) {
|
||||
if (extraItems.length) {
|
||||
menu.append(new MenuItem({
|
||||
label: item.label,
|
||||
click: async () => {
|
||||
const args = item.commandArgs || [];
|
||||
void CommandService.instance().execute(item.commandName, ...args);
|
||||
},
|
||||
type: item.type,
|
||||
type: 'separator',
|
||||
}));
|
||||
}
|
||||
|
||||
for (const extraItem of extraItems) {
|
||||
menu.append(extraItem);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line github/array-foreach, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
menuUtils.pluginContextMenuItems(props.plugins, MenuItemLocation.EditorContextMenu).forEach((item: any) => {
|
||||
menu.append(new MenuItem(item));
|
||||
});
|
||||
|
||||
menu.popup({ window: bridge().activeWindow() });
|
||||
}
|
||||
menu.popup({ window: targetWindow });
|
||||
};
|
||||
|
||||
// Prepend the event listener so that it gets called before
|
||||
// the listener that shows the default menu.
|
||||
const targetWindow = bridge().windowById(windowId);
|
||||
targetWindow.webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
return () => {
|
||||
@@ -167,7 +311,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
}
|
||||
};
|
||||
}, [
|
||||
props.plugins, props.editorClassName, editorRef, props.containerRef,
|
||||
props.plugins, props.dispatch, props.editorClassName, editorRef, props.containerRef,
|
||||
props.editorCutText, props.editorCopyText, props.editorPaste,
|
||||
windowId,
|
||||
]);
|
||||
|
||||
@@ -722,6 +722,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
|
||||
useContextMenu({
|
||||
plugins: props.plugins,
|
||||
dispatch: props.dispatch,
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
editorRef,
|
||||
editorClassName: 'codeMirrorEditor',
|
||||
|
||||
@@ -303,6 +303,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
|
||||
useContextMenu({
|
||||
plugins: props.plugins,
|
||||
dispatch: props.dispatch,
|
||||
editorCutText, editorCopyText, editorPaste,
|
||||
editorRef,
|
||||
editorClassName: 'cm-editor',
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef, RefObject } from 'react';
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { EditorProps, LogMessageCallback, OnEventCallback, ContentScriptData } from '@joplin/editor/types';
|
||||
import { EditorProps, LogMessageCallback, OnEventCallback } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
|
||||
import { dirname } from 'path';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { SearchMarkers } from '../../../utils/useSearchMarkers';
|
||||
@@ -18,6 +14,7 @@ import Resource from '@joplin/lib/models/Resource';
|
||||
import { parseResourceUrl } from '@joplin/lib/urlUtils';
|
||||
import { resourceFilename } from '@joplin/lib/models/utils/resourceUtils';
|
||||
import getResourceBaseUrl from '../../../utils/getResourceBaseUrl';
|
||||
import useContentScriptRegistration from './utils/useContentScriptRegistration';
|
||||
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
@@ -64,37 +61,7 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
return editor;
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScripts: ContentScriptData[] = [];
|
||||
for (const pluginId in props.pluginStates) {
|
||||
const pluginState = props.pluginStates[pluginId];
|
||||
const codeMirrorContentScripts = pluginState.contentScripts[ContentScriptType.CodeMirrorPlugin] ?? [];
|
||||
|
||||
for (const contentScript of codeMirrorContentScripts) {
|
||||
contentScripts.push({
|
||||
pluginId,
|
||||
contentScriptId: contentScript.id,
|
||||
contentScriptJs: () => shim.fsDriver().readFile(contentScript.path),
|
||||
loadCssAsset: (name: string) => {
|
||||
const assetPath = dirname(contentScript.path);
|
||||
const path = shim.fsDriver().resolveRelativePathWithinDir(assetPath, name);
|
||||
return shim.fsDriver().readFile(path, 'utf8');
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
postMessageHandler: (message: any) => {
|
||||
const plugin = PluginService.instance().pluginById(pluginId);
|
||||
return plugin.emitContentScriptMessage(contentScript.id, message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void editor.setContentScripts(contentScripts);
|
||||
}, [editor, props.pluginStates]);
|
||||
useContentScriptRegistration({ editor, pluginStates: props.pluginStates });
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainerRef.current) return () => {};
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import { ContentScriptData, ContentScriptLoadOptions } from '@joplin/editor/types';
|
||||
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { dirname } from 'path';
|
||||
import { useEffect, useId, useRef } from 'react';
|
||||
import bridge from '../../../../../../services/bridge';
|
||||
import type { ContentScriptRegistration } from '../../../../../../utils/customProtocols/handleCustomProtocols';
|
||||
|
||||
interface Props {
|
||||
editor: CodeMirrorControl;
|
||||
pluginStates: PluginStates;
|
||||
}
|
||||
|
||||
const useContentScriptRegistration = ({ editor, pluginStates }: Props) => {
|
||||
const loadedContentScriptRefs = useRef(new Map<string, ContentScriptRegistration>());
|
||||
|
||||
const editorId = useId();
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScripts: ContentScriptData[] = [];
|
||||
for (const pluginId in pluginStates) {
|
||||
const pluginState = pluginStates[pluginId];
|
||||
const codeMirrorContentScripts = pluginState.contentScripts[ContentScriptType.CodeMirrorPlugin] ?? [];
|
||||
|
||||
for (const contentScript of codeMirrorContentScripts) {
|
||||
// Ensure that the key is unique to the (pluginId, editorId, contentScript) set.
|
||||
// Include the plugin ID to prevent ID collisions if multiple plugins register
|
||||
// content scripts with the same ID:
|
||||
const scriptId = `${pluginId}::${contentScript.id}`;
|
||||
loadedContentScriptRefs.current.get(scriptId)?.revoke();
|
||||
|
||||
contentScripts.push({
|
||||
pluginId,
|
||||
contentScriptId: contentScript.id,
|
||||
contentScriptJs: async (context) => {
|
||||
const handle = await registerContentScriptWithMainProcess({
|
||||
scriptPath: contentScript.path,
|
||||
context,
|
||||
|
||||
key: `${editorId}::${scriptId}`,
|
||||
});
|
||||
loadedContentScriptRefs.current.set(scriptId, handle);
|
||||
|
||||
return { uri: handle.uri };
|
||||
},
|
||||
loadCssAsset: (name: string) => {
|
||||
const assetPath = dirname(contentScript.path);
|
||||
const path = shim.fsDriver().resolveRelativePathWithinDir(assetPath, name);
|
||||
return shim.fsDriver().readFile(path, 'utf8');
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
postMessageHandler: (message: any) => {
|
||||
const plugin = PluginService.instance().pluginById(pluginId);
|
||||
return plugin.emitContentScriptMessage(contentScript.id, message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void editor.setContentScripts(contentScripts);
|
||||
}, [editor, pluginStates, editorId]);
|
||||
|
||||
useEffect(() => () => {
|
||||
for (const script of loadedContentScriptRefs.current.values()) {
|
||||
script.revoke();
|
||||
}
|
||||
loadedContentScriptRefs.current.clear();
|
||||
}, []);
|
||||
};
|
||||
|
||||
interface RegisterContentScriptOptions {
|
||||
key: string; // A unique identifier for the content script
|
||||
scriptPath: string;
|
||||
context: ContentScriptLoadOptions;
|
||||
}
|
||||
|
||||
const registerContentScriptWithMainProcess = async (
|
||||
{ key, scriptPath, context }: RegisterContentScriptOptions,
|
||||
) => {
|
||||
const contentScriptJs = [
|
||||
context.contentScriptStartJs,
|
||||
await shim.fsDriver().readFile(scriptPath),
|
||||
context.contentScriptEndJs,
|
||||
].join('\n');
|
||||
|
||||
const content = bridge().electronApp().getPluginProtocolHandler().registerContentScript(
|
||||
encodeURIComponent(key),
|
||||
contentScriptJs,
|
||||
);
|
||||
return content;
|
||||
};
|
||||
|
||||
export default useContentScriptRegistration;
|
||||
@@ -40,7 +40,7 @@ const supportedLocales = require('./supportedLocales');
|
||||
import { hasProtocol } from '@joplin/utils/url';
|
||||
import useTabIndenter from './utils/useTabIndenter';
|
||||
import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
|
||||
import useDocument from '../../../hooks/useDocument';
|
||||
import useDocument from '@joplin/lib/hooks/dom/useDocument';
|
||||
import useEditDialog from './utils/useEditDialog';
|
||||
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import { ContextMenuOptions, ContextMenuItemType } from '../../../utils/contextMenuUtils';
|
||||
import { ContextMenuOptions, ContextMenuItemType, buildMenuItems } from '../../../utils/contextMenuUtils';
|
||||
import { menuItems } from '../../../utils/contextMenu';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -38,7 +38,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
const contextMenuItems = menuItems(dispatch);
|
||||
const targetWindow = bridge().windowById(windowId);
|
||||
|
||||
const makeMainMenuItems = (element: Element) => {
|
||||
const makeMainMenuItems = async (element: Element) => {
|
||||
let itemType: ContextMenuItemType = ContextMenuItemType.None;
|
||||
let resourceId = '';
|
||||
let linkUrl = null;
|
||||
@@ -79,20 +79,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
mdToHtml,
|
||||
};
|
||||
|
||||
const result = [];
|
||||
for (const itemName in contextMenuItems) {
|
||||
const item = contextMenuItems[itemName];
|
||||
|
||||
if (!item.isActive(itemType, contextMenuActionOptions.current)) continue;
|
||||
|
||||
result.push(new MenuItem({
|
||||
label: item.label,
|
||||
click: () => {
|
||||
item.onAction(contextMenuActionOptions.current);
|
||||
},
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
return buildMenuItems(contextMenuItems, contextMenuActionOptions.current);
|
||||
};
|
||||
|
||||
const makeEditableMenuItems = (element: Element) => {
|
||||
@@ -111,7 +98,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
return [];
|
||||
};
|
||||
|
||||
const showContextMenu = (element: HTMLElement, misspelledWord: string|null, dictionarySuggestions: string[]) => {
|
||||
const showContextMenu = async (element: HTMLElement, misspelledWord: string|null, dictionarySuggestions: string[]) => {
|
||||
const menu = new Menu();
|
||||
const menuItems: MenuItemType[] = [];
|
||||
const toMenuItems = (specs: MenuItemConstructorOptions[]) => {
|
||||
@@ -119,7 +106,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
};
|
||||
|
||||
menuItems.push(...makeEditableMenuItems(element));
|
||||
menuItems.push(...makeMainMenuItems(element));
|
||||
menuItems.push(...(await makeMainMenuItems(element)));
|
||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
|
||||
menuItems.push(
|
||||
...toMenuItems(spellCheckerMenuItems),
|
||||
@@ -135,16 +122,16 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
};
|
||||
|
||||
let lastTarget: EventTarget|null = null;
|
||||
const onElectronContextMenu = (event: ElectronEvent, params: ContextMenuParams) => {
|
||||
const onElectronContextMenu = async (event: ElectronEvent, params: ContextMenuParams) => {
|
||||
if (!lastTarget) return;
|
||||
const element = lastTarget as HTMLElement;
|
||||
lastTarget = null;
|
||||
|
||||
event.preventDefault();
|
||||
showContextMenu(element, params.misspelledWord, params.dictionarySuggestions);
|
||||
await showContextMenu(element, params.misspelledWord, params.dictionarySuggestions);
|
||||
};
|
||||
|
||||
const onBrowserContextMenu = (event: PointerEvent) => {
|
||||
const onBrowserContextMenu = async (event: PointerEvent) => {
|
||||
const isKeyboard = event.buttons === 0;
|
||||
if (isKeyboard) {
|
||||
// Context menu events from the keyboard seem to always use <body> as the
|
||||
@@ -163,7 +150,7 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Dispatc
|
||||
const isFromPlugin = !event.isTrusted;
|
||||
if (isFromPlugin) {
|
||||
event.preventDefault();
|
||||
showContextMenu(lastTarget as HTMLElement, null, []);
|
||||
await showContextMenu(lastTarget as HTMLElement, null, []);
|
||||
lastTarget = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { copyHtmlToClipboard } from './clipboardUtils';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng, svgDimensions } from './contextMenuUtils';
|
||||
import { ContextMenuItemType, ContextMenuOptions, ContextMenuItems, resourceInfo, textToDataUri, svgUriToPng, svgDimensions, buildMenuItems, ContextMenuItem } from './contextMenuUtils';
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import Resource, { resourceOcrStatusToString } from '@joplin/lib/models/Resource';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
@@ -82,6 +81,15 @@ export async function openItemById(itemId: string, dispatch: Function, hash = ''
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
const makeSeparator = (): ContextMenuItem => {
|
||||
return {
|
||||
isActive: () => { return true; },
|
||||
label: '',
|
||||
onAction: () => {},
|
||||
isSeparator: true,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
open: {
|
||||
label: _('Open...'),
|
||||
@@ -138,6 +146,16 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !!options.textToCopy && itemType === ContextMenuItemType.Image && options.mime?.startsWith('image/svg'),
|
||||
},
|
||||
separator1: makeSeparator(),
|
||||
revealInFolder: {
|
||||
label: _('Reveal file in folder'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
const { resourcePath } = await resourceInfo(options);
|
||||
bridge().showItemInFolder(resourcePath);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && (itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource),
|
||||
},
|
||||
separator2: makeSeparator(),
|
||||
recognizeHandwrittenImage: {
|
||||
label: _('Recognize handwritten image'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
@@ -172,14 +190,6 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
return itemType === ContextMenuItemType.Resource || (itemType === ContextMenuItemType.Image && options.resourceId);
|
||||
},
|
||||
},
|
||||
revealInFolder: {
|
||||
label: _('Reveal file in folder'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
const { resourcePath } = await resourceInfo(options);
|
||||
bridge().showItemInFolder(resourcePath);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image || itemType === ContextMenuItemType.Resource,
|
||||
},
|
||||
copyOcrText: {
|
||||
label: _('View OCR text'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
@@ -197,6 +207,7 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
return itemType === ContextMenuItemType.Resource || (itemType === ContextMenuItemType.Image && options.resourceId);
|
||||
},
|
||||
},
|
||||
separator3: makeSeparator(),
|
||||
copyPathToClipboard: {
|
||||
label: _('Copy path to clipboard'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
@@ -221,6 +232,14 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.textToCopy && itemType === ContextMenuItemType.Image,
|
||||
},
|
||||
copyLinkUrl: {
|
||||
label: _('Copy Link Address'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
clipboard.writeText(options.linkToCopy !== null ? options.linkToCopy : options.textToCopy);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType === ContextMenuItemType.Link || !!options.linkToCopy,
|
||||
},
|
||||
separator4: makeSeparator(),
|
||||
cut: {
|
||||
label: _('Cut'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
@@ -250,13 +269,6 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
},
|
||||
isActive: (_itemType: ContextMenuItemType, options: ContextMenuOptions) => !options.isReadOnly && (!!clipboard.readText() || !!clipboard.readHTML()),
|
||||
},
|
||||
copyLinkUrl: {
|
||||
label: _('Copy Link Address'),
|
||||
onAction: async (options: ContextMenuOptions) => {
|
||||
clipboard.writeText(options.linkToCopy !== null ? options.linkToCopy : options.textToCopy);
|
||||
},
|
||||
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => itemType === ContextMenuItemType.Link || !!options.linkToCopy,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -264,20 +276,12 @@ export function menuItems(dispatch: Function): ContextMenuItems {
|
||||
export default async function contextMenu(options: ContextMenuOptions, dispatch: Function) {
|
||||
const menu = new Menu();
|
||||
|
||||
const items = menuItems(dispatch);
|
||||
|
||||
if (!('readyOnly' in options)) options.isReadOnly = true;
|
||||
for (const itemKey in items) {
|
||||
const item = items[itemKey];
|
||||
|
||||
if (!item.isActive(options.itemType, options)) continue;
|
||||
const items = await buildMenuItems(menuItems(dispatch), options);
|
||||
|
||||
menu.append(new MenuItem({
|
||||
label: item.label,
|
||||
click: () => {
|
||||
item.onAction(options);
|
||||
},
|
||||
}));
|
||||
for (const item of items) {
|
||||
menu.append(item);
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import bridge from '../../../services/bridge';
|
||||
import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from './types';
|
||||
import { ContextMenuItemType, EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/types';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { type MenuItem as MenuItemType } from 'electron';
|
||||
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const logger = Logger.create('contextMenuUtils');
|
||||
|
||||
export enum ContextMenuItemType {
|
||||
None = '',
|
||||
Image = 'image',
|
||||
Resource = 'resource',
|
||||
Text = 'text',
|
||||
Link = 'link',
|
||||
}
|
||||
// Re-export for backward compatibility
|
||||
export { ContextMenuItemType };
|
||||
|
||||
export interface ContextMenuOptions {
|
||||
itemType: ContextMenuItemType;
|
||||
@@ -36,6 +37,7 @@ export interface ContextMenuItem {
|
||||
onAction: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
isActive: Function;
|
||||
isSeparator?: boolean;
|
||||
}
|
||||
|
||||
export interface ContextMenuItems {
|
||||
@@ -127,3 +129,101 @@ export const svgUriToPng = (document: Document, svg: string, width: number, heig
|
||||
img.src = svg;
|
||||
});
|
||||
};
|
||||
|
||||
// Filter out leading, trailing, and consecutive separators from a list
|
||||
const filterSeparators = <T>(items: T[], isSeparator: (item: T)=> boolean): T[] => {
|
||||
const filtered: T[] = [];
|
||||
let lastWasSeparator = true;
|
||||
for (const item of items) {
|
||||
if (isSeparator(item)) {
|
||||
if (lastWasSeparator) continue;
|
||||
lastWasSeparator = true;
|
||||
} else {
|
||||
lastWasSeparator = false;
|
||||
}
|
||||
filtered.push(item);
|
||||
}
|
||||
|
||||
while (filtered.length > 0 && isSeparator(filtered[filtered.length - 1])) {
|
||||
filtered.pop();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
export interface EditorContextMenuFilterContext {
|
||||
resourceId?: string;
|
||||
itemType?: ContextMenuItemType;
|
||||
textToCopy?: string;
|
||||
}
|
||||
|
||||
export const handleEditorContextMenuFilter = async (context?: EditorContextMenuFilterContext) => {
|
||||
let filterObject: EditContextMenuFilterObject = {
|
||||
items: [],
|
||||
context,
|
||||
};
|
||||
|
||||
filterObject = await eventManager.filterEmit('editorContextMenu', filterObject);
|
||||
|
||||
const filteredItems = filterSeparators(filterObject.items, item => item.type === 'separator');
|
||||
|
||||
const output: MenuItemType[] = [];
|
||||
for (const item of filteredItems) {
|
||||
output.push(new MenuItem({
|
||||
label: item.label,
|
||||
click: async () => {
|
||||
const args = item.commandArgs || [];
|
||||
void CommandService.instance().execute(item.commandName, ...args);
|
||||
},
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export const buildMenuItems = async (items: ContextMenuItems, options: ContextMenuOptions) => {
|
||||
const activeItems: ContextMenuItem[] = [];
|
||||
for (const itemKey in items) {
|
||||
const item = items[itemKey];
|
||||
if (item.isActive(options.itemType, options)) {
|
||||
activeItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const extraItems = await handleEditorContextMenuFilter({
|
||||
resourceId: options.resourceId,
|
||||
itemType: options.itemType,
|
||||
textToCopy: options.textToCopy,
|
||||
});
|
||||
|
||||
if (extraItems.length) {
|
||||
activeItems.push({
|
||||
isActive: () => true,
|
||||
label: '',
|
||||
onAction: () => {},
|
||||
isSeparator: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const [, extraItem] of extraItems.entries()) {
|
||||
activeItems.push({
|
||||
isActive: () => true,
|
||||
label: extraItem.label,
|
||||
onAction: () => {
|
||||
extraItem.click();
|
||||
},
|
||||
isSeparator: extraItem.type === 'separator',
|
||||
});
|
||||
}
|
||||
|
||||
const filteredItems = filterSeparators(activeItems, item => item.isSeparator);
|
||||
|
||||
return filteredItems.map(item => new MenuItem({
|
||||
label: item.label,
|
||||
click: () => {
|
||||
item.onAction(options);
|
||||
},
|
||||
type: item.isSeparator ? 'separator' : 'normal',
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
@@ -37,7 +37,7 @@ describe('useFormNote', () => {
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(),
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
// id is falsy until after the first load of the form note.
|
||||
expect(formNote.result.current.formNote.id).not.toBeFalsy();
|
||||
});
|
||||
@@ -55,11 +55,9 @@ describe('useFormNote', () => {
|
||||
});
|
||||
|
||||
// Changing encryption_applied should cause a re-render
|
||||
await act(async () => {
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 1,
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +70,7 @@ describe('useFormNote', () => {
|
||||
});
|
||||
|
||||
// Ending decryption should also cause a re-render
|
||||
await formNote.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
});
|
||||
@@ -98,7 +96,7 @@ describe('useFormNote', () => {
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(),
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
is_conflict: 1,
|
||||
title: testNote.title,
|
||||
@@ -127,7 +125,7 @@ describe('useFormNote', () => {
|
||||
initialProps: props,
|
||||
});
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(formNote.result.current.formNote.title).toBe('Test Note!');
|
||||
});
|
||||
|
||||
@@ -136,7 +134,7 @@ describe('useFormNote', () => {
|
||||
await Note.save({ id: note.id, title: 'Modified' });
|
||||
});
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(formNote.result.current.formNote.title).toBe('Modified');
|
||||
});
|
||||
|
||||
@@ -160,8 +158,8 @@ describe('useFormNote', () => {
|
||||
initialProps: makeFormNoteProps(),
|
||||
});
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
return Object.values(formNote.result.current.resourceInfos).length > 0;
|
||||
await waitFor(() => {
|
||||
expect(Object.values(formNote.result.current.resourceInfos).length).toBeGreaterThan(0);
|
||||
});
|
||||
const initialResourceInfos = formNote.result.current.resourceInfos;
|
||||
expect(initialResourceInfos).toMatchObject({
|
||||
@@ -171,7 +169,7 @@ describe('useFormNote', () => {
|
||||
await act(async () => {
|
||||
await Resource.save({ ...resource, filename: 'test.txt' });
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
const resourceInfo = formNote.result.current.resourceInfos[resource.id];
|
||||
expect(resourceInfo.item).toMatchObject({
|
||||
id: resource.id, filename: 'test.txt',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import usePluginEditorView from './usePluginEditorView';
|
||||
import { PluginEditorViewState, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
|
||||
@@ -217,7 +217,7 @@ const NoteList = (props: Props) => {
|
||||
const renderNotes = () => {
|
||||
if (!props.notes.length) return [];
|
||||
|
||||
const output: JSX.Element[] = [];
|
||||
const output: React.ReactNode[] = [];
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
const note = props.notes[i];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import useVisibleRange from './useVisibleRange';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
|
||||
describe('useVisibleRange', () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { NoteListColumns, OnClickHandler } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
import NoteListHeaderItem from './NoteListHeaderItem';
|
||||
import { OnItemClickHander } from './types';
|
||||
import useDragAndDrop, { DataType } from './useDragAndDrop';
|
||||
@@ -53,7 +52,7 @@ export default (props: Props) => {
|
||||
const itemHeight = props.height ? props.height : defaultHeight;
|
||||
|
||||
const style = useMemo(() => {
|
||||
return { height: itemHeight } as CSSProperties;
|
||||
return { height: itemHeight };
|
||||
}, [itemHeight]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import { ColumnName } from '@joplin/lib/services/plugins/api/noteListType';
|
||||
|
||||
const titles: Record<ColumnName, ()=> string> = {
|
||||
'note.checkboxes': () => _('Checkbox completion'),
|
||||
'note.folder.title': () => _('Notebook: %s', _('Title')),
|
||||
'note.is_todo': () => _('To-do'),
|
||||
'note.latitude': () => _('Latitude'),
|
||||
@@ -16,6 +17,7 @@ const titles: Record<ColumnName, ()=> string> = {
|
||||
};
|
||||
|
||||
const titlesForHeader: Partial<Record<ColumnName, ()=> string>> = {
|
||||
'note.checkboxes': () => '◐',
|
||||
'note.is_todo': () => '✓',
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/databa
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import prepareViewProps from './prepareViewProps';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
// Same as `prepareViewProps` but with default arguments to make testing code simpler.
|
||||
@@ -98,4 +99,67 @@ describe('prepareViewProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return checkbox stats only when setting is enabled', async () => {
|
||||
const note = await Note.save({
|
||||
title: 'test',
|
||||
body: '- [ ] task 1\n- [x] task 2\n- [ ] task 3\n- [X] task 4',
|
||||
});
|
||||
|
||||
Setting.setValue('notes.showCheckboxCompletionChart', true);
|
||||
expect(await prepare(['note.checkboxes'], note)).toEqual({
|
||||
note: {
|
||||
checkboxes: {
|
||||
total: 4,
|
||||
checked: 2,
|
||||
percent: 50,
|
||||
isComplete: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Setting.setValue('notes.showCheckboxCompletionChart', false);
|
||||
expect(await prepare(['note.checkboxes'], note)).toEqual({
|
||||
note: {
|
||||
checkboxes: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for checkbox stats when note has no checkboxes', async () => {
|
||||
Setting.setValue('notes.showCheckboxCompletionChart', true);
|
||||
|
||||
const note = await Note.save({
|
||||
title: 'test',
|
||||
body: 'This is a note without any checkboxes.',
|
||||
});
|
||||
|
||||
const result = await prepare(['note.checkboxes'], note);
|
||||
expect(result).toEqual({
|
||||
note: {
|
||||
checkboxes: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return isComplete true when all checkboxes are checked', async () => {
|
||||
Setting.setValue('notes.showCheckboxCompletionChart', true);
|
||||
|
||||
const note = await Note.save({
|
||||
title: 'test',
|
||||
body: '- [x] task 1\n- [X] task 2\n- [x] task 3',
|
||||
});
|
||||
|
||||
const result = await prepare(['note.checkboxes'], note);
|
||||
expect(result).toEqual({
|
||||
note: {
|
||||
checkboxes: {
|
||||
total: 3,
|
||||
checked: 3,
|
||||
percent: 100,
|
||||
isComplete: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -2,8 +2,37 @@ import { ListRendererDependency } from '@joplin/lib/services/plugins/api/noteLis
|
||||
import { FolderEntity, NoteEntity, TagEntity } from '@joplin/lib/services/database/types';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface CheckboxStats {
|
||||
total: number;
|
||||
checked: number;
|
||||
percent: number;
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
const countCheckboxes = (body: string): CheckboxStats | null => {
|
||||
if (!body) return null;
|
||||
|
||||
// Match unchecked: - [ ] and checked: - [x] or - [X]
|
||||
const uncheckedMatches = body.match(/- \[ \]/g);
|
||||
const checkedMatches = body.match(/- \[[xX]\]/g);
|
||||
|
||||
const unchecked = uncheckedMatches ? uncheckedMatches.length : 0;
|
||||
const checked = checkedMatches ? checkedMatches.length : 0;
|
||||
const total = unchecked + checked;
|
||||
|
||||
if (total === 0) return null;
|
||||
|
||||
return {
|
||||
total,
|
||||
checked,
|
||||
percent: Math.round((checked / total) * 100),
|
||||
isComplete: checked === total,
|
||||
};
|
||||
};
|
||||
|
||||
const prepareViewProps = async (
|
||||
dependencies: ListRendererDependency[],
|
||||
note: NoteEntity,
|
||||
@@ -40,6 +69,14 @@ const prepareViewProps = async (
|
||||
taskStatus = note.todo_completed ? _('Complete to-do') : _('Incomplete to-do');
|
||||
}
|
||||
output.note[propName] = taskStatus;
|
||||
} else if (dep === 'note.checkboxes') {
|
||||
// Only load the note body and compute checkbox stats if the setting is enabled
|
||||
if (Setting.value('notes.showCheckboxCompletionChart')) {
|
||||
if (!('body' in note)) note = await Note.load(note.id);
|
||||
output.note[propName] = countCheckboxes(note.body);
|
||||
} else {
|
||||
output.note[propName] = null;
|
||||
}
|
||||
} else {
|
||||
// The notes in the state only contain the properties defined in
|
||||
// Note.previewFields(). It means that if a view request a
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import useRootElement from './useRootElement';
|
||||
|
||||
describe('useRootElement', () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import bridge from '../services/bridge';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from './Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import { formatDateTimeLocalToMs, formatMsToDateTimeLocal, formatMsToLocal } from '@joplin/utils/time';
|
||||
const { clipboard } = require('electron');
|
||||
const formatcoords = require('formatcoords');
|
||||
|
||||
@@ -5,7 +5,7 @@ import bridge from '../services/bridge';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { WindowIdContext } from './NewWindowOrIFrame';
|
||||
import useDocument from './hooks/useDocument';
|
||||
import useDocument from '@joplin/lib/hooks/dom/useDocument';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import getAssetPath from '../utils/getAssetPath';
|
||||
import { toForwardSlashes } from '@joplin/utils/path';
|
||||
@@ -86,7 +86,7 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
|
||||
const result: NoteViewerControl = {
|
||||
domReady: () => domReadyRef.current,
|
||||
setHtml: (html: string, options: SetHtmlOptions) => {
|
||||
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
|
||||
const protocolHandler = bridge().electronApp().getContentProtocolHandler();
|
||||
|
||||
// Grant & remove asset access.
|
||||
if (options.pluginAssets) {
|
||||
|
||||
52
packages/app-desktop/gui/PromptDialog.test.ts
Normal file
52
packages/app-desktop/gui/PromptDialog.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import PromptDialog from './PromptDialog';
|
||||
|
||||
describe('PromptDialog Escape key handling', () => {
|
||||
|
||||
const setupKeyHandler = (inputType: string, menuIsOpened: boolean) => {
|
||||
const onCloseMock = jest.fn();
|
||||
const instance = new PromptDialog({
|
||||
themeId: 1,
|
||||
defaultValue: '',
|
||||
visible: true,
|
||||
buttons: ['ok', 'cancel'],
|
||||
onClose: onCloseMock,
|
||||
inputType,
|
||||
description: '',
|
||||
autocomplete: [],
|
||||
label: 'Test',
|
||||
answer: null,
|
||||
});
|
||||
instance.state = { visible: true, answer: 'some-answer' };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private property for test setup
|
||||
(instance as unknown as any).menuIsOpened_ = menuIsOpened;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Navigating React element tree for handler extraction
|
||||
const rendered = instance.render() as any;
|
||||
const dialogChildren = rendered.props.children;
|
||||
const inputWrapper = dialogChildren[1];
|
||||
const wrapperChildren = inputWrapper.props.children;
|
||||
const inputComp = Array.isArray(wrapperChildren)
|
||||
? wrapperChildren[0]
|
||||
: wrapperChildren;
|
||||
|
||||
return { onKeyDown: inputComp.props.onKeyDown, onCloseMock };
|
||||
};
|
||||
|
||||
test('closes dialog for text input', () => {
|
||||
const { onKeyDown, onCloseMock } = setupKeyHandler('text', false);
|
||||
onKeyDown({ key: 'Escape' });
|
||||
expect(onCloseMock).toHaveBeenCalledWith(null, 'cancel');
|
||||
});
|
||||
|
||||
test('closes dialog for dropdown when menu is closed', () => {
|
||||
const { onKeyDown, onCloseMock } = setupKeyHandler('dropdown', false);
|
||||
onKeyDown({ key: 'Escape' });
|
||||
expect(onCloseMock).toHaveBeenCalledWith(null, 'cancel');
|
||||
});
|
||||
|
||||
test('does not close dialog when react-select menu is open', () => {
|
||||
const { onKeyDown, onCloseMock } = setupKeyHandler('dropdown', true);
|
||||
onKeyDown({ key: 'Escape' });
|
||||
expect(onCloseMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import CreatableSelect from 'react-select/creatable';
|
||||
import Select from 'react-select';
|
||||
import makeAnimated from 'react-select/animated';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from './Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { formatDateTimeLocalToMs, isValidDate } from '@joplin/utils/time';
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
@@ -251,6 +251,16 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
} else {
|
||||
onClose(true);
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
// react-select calls preventDefault() on the Escape keydown
|
||||
// event, which prevents the native <dialog> cancel event from
|
||||
// firing. We handle Escape explicitly here to ensure the dialog
|
||||
// can be closed with the Escape key.
|
||||
if ((this.props.inputType === 'tags' || this.props.inputType === 'dropdown') && this.menuIsOpened_) {
|
||||
// Let react-select close the dropdown menu
|
||||
} else {
|
||||
onClose(false, 'cancel');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -268,10 +278,10 @@ export default class PromptDialog extends React.Component<Props, any> {
|
||||
/>;
|
||||
} else if (this.props.inputType === 'tags') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
inputComp = <CreatableSelect className="tag-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated()} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
inputComp = <CreatableSelect className="tag-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} value={this.state.answer} placeholder="" components={makeAnimated() as any} isMulti={true} isClearable={false} backspaceRemovesValue={true} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
} else if (this.props.inputType === 'dropdown') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
inputComp = <Select className="item-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated()} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
inputComp = <Select className="item-selector" onMenuOpen={this.select_menuOpen} onMenuClose={this.select_menuClose} styles={styles.select} theme={styles.selectTheme} ref={this.answerInput_} components={makeAnimated() as any} value={this.props.answer} defaultValue={this.props.defaultValue} isClearable={false} options={this.props.autocomplete} onChange={onSelectChange} onKeyDown={(event: any) => onKeyDown(event)} />;
|
||||
} else {
|
||||
inputComp = <input style={styles.input} ref={this.answerInput_} value={this.state.answer} type="text" onChange={event => onChange(event)} onKeyDown={event => onKeyDown(event)} />;
|
||||
}
|
||||
|
||||
80
packages/app-desktop/gui/QuitSyncDialog.tsx
Normal file
80
packages/app-desktop/gui/QuitSyncDialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import bridge from '../services/bridge';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogButtonRow, { ClickEvent } from './DialogButtonRow';
|
||||
import styled from 'styled-components';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import Synchronizer from '@joplin/lib/Synchronizer';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
const StyledContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
min-width: 300px;
|
||||
`;
|
||||
|
||||
const StyledMessage = styled.div`
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default function QuitSyncDialog(props: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const showDialog = useSelector((state: AppState) => state.showQuitSyncDialog);
|
||||
const syncPending = useSelector((state: AppState) => state.syncPending);
|
||||
const syncStarted = useSelector((state: AppState) => state.syncStarted);
|
||||
|
||||
// Auto-quit when sync completes
|
||||
useEffect(() => {
|
||||
if (showDialog && !syncPending) {
|
||||
dispatch({ type: 'QUIT_SYNC_DIALOG_CLOSE' });
|
||||
void bridge().electronApp().quit();
|
||||
}
|
||||
}, [showDialog, syncPending, dispatch]);
|
||||
|
||||
// Trigger immediate sync when dialog opens if not already syncing
|
||||
// If the sync process schedules another sync automatically, this hook will trigger another sync immediately, instead of having to wait the syncAsYouTypeInterval
|
||||
// That is because this triggers when sync completed is emitted, but if another sync is not scheduled, the app quits via the other hook when syncPending is reset to false just after
|
||||
useEffect(() => {
|
||||
if (showDialog && syncPending && !syncStarted) {
|
||||
void reg.scheduleSync(0, { syncSteps: Synchronizer.partialSyncSteps });
|
||||
}
|
||||
}, [showDialog, syncPending, syncStarted]);
|
||||
|
||||
if (!showDialog) return null;
|
||||
|
||||
const handleCancel = (_event: ClickEvent) => {
|
||||
dispatch({ type: 'QUIT_SYNC_DIALOG_CLOSE' });
|
||||
};
|
||||
|
||||
const handleQuitAnyway = (_event: ClickEvent) => {
|
||||
dispatch({ type: 'QUIT_SYNC_DIALOG_CLOSE' });
|
||||
void bridge().electronApp().quit();
|
||||
};
|
||||
|
||||
const dialogButtonOnClick = (event: ClickEvent) => {
|
||||
if (event.buttonName === 'ok') {
|
||||
handleQuitAnyway(event);
|
||||
} else if (event.buttonName === 'cancel') {
|
||||
handleCancel(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<StyledContent>
|
||||
<StyledMessage>{_('Synchronising remaining changes, please wait...')}</StyledMessage>
|
||||
<DialogButtonRow themeId={props.themeId} onClick={dialogButtonOnClick} okButtonLabel={_('Quit anyway')} cancelButtonLabel={_('Cancel')} />
|
||||
</StyledContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { canMove, MoveDirection } from './utils/movements';
|
||||
import MoveButtons, { MoveButtonClickEvent } from './MoveButtons';
|
||||
import { StyledWrapperRoot, StyledMoveOverlay, MoveModeRootMessage } from './utils/style';
|
||||
import type { ResizeCallback, ResizeStartCallback } from 're-resizable';
|
||||
import Dialog from '../Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import EventEmitter = require('events');
|
||||
import LayoutItemContainer from './LayoutItemContainer';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useLayoutItemSizes, { itemSize, calculateMaxSizeAvailableForItem } from './useLayoutItemSizes';
|
||||
import { LayoutItem, LayoutItemDirection } from './types';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import validateLayout from './validateLayout';
|
||||
|
||||
describe('useLayoutItemSizes', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import ClipperServer from '@joplin/lib/ClipperServer';
|
||||
import DialogTitle from './DialogTitle';
|
||||
import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
|
||||
import Dialog from './Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
import ImportScreen from './ImportScreen';
|
||||
import ResourceScreen from './ResourceScreen';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import Dialog from '../Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import DialogButtonRow from './DialogButtonRow';
|
||||
import Dialog from './Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import DialogTitle from './DialogTitle';
|
||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||
import { StateShare } from '@joplin/lib/services/share/reducer';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import useDocument from '../hooks/useDocument';
|
||||
import useDocument from '@joplin/lib/hooks/dom/useDocument';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import { useRef, useCallback, useId } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import DialogButtonRow from '../DialogButtonRow';
|
||||
import Dialog from '../Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import styled from 'styled-components';
|
||||
import DialogTitle from '../DialogTitle';
|
||||
import SyncTargetRegistry, { SyncTargetInfo } from '@joplin/lib/SyncTargetRegistry';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import Dialog from '../Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
|
||||
interface Props {
|
||||
message: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Dispatch } from 'redux';
|
||||
import ModalMessageOverlay from './ModalMessageOverlay';
|
||||
import { EditorNoteStatuses, stateUtils } from '@joplin/lib/reducer';
|
||||
import dialogs from '../dialogs';
|
||||
import useDocument from '../hooks/useDocument';
|
||||
import useDocument from '@joplin/lib/hooks/dom/useDocument';
|
||||
import useWindowCommands from './utils/useWindowCommands';
|
||||
import PluginDialogs from './PluginDialogs';
|
||||
import useSyncDialogState from './utils/useSyncDialogState';
|
||||
|
||||
@@ -14,12 +14,12 @@ export const declaration: CommandDeclaration = {
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext, body = '', isTodo = false) => {
|
||||
const folderId = await Folder.getValidActiveFolder();
|
||||
if (!folderId) return;
|
||||
const folder = await Folder.getValidActiveFolder();
|
||||
if (!folder) return;
|
||||
|
||||
const defaultValues = Note.previewFieldsWithDefaultValues({ includeTimestamps: false });
|
||||
|
||||
let newNote = { ...defaultValues, parent_id: folderId,
|
||||
let newNote = { ...defaultValues, parent_id: folder.id,
|
||||
is_todo: isTodo ? 1 : 0,
|
||||
body: body };
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Options {
|
||||
}
|
||||
|
||||
const showFolderPicker = async (control: WindowControl, { label, allowSelectNone, showFolder }: Options) => {
|
||||
const folders = await Folder.sortFolderTree();
|
||||
const folders = await Folder.sortFolderTree(null, { includeDeleted: false });
|
||||
const startFolders: FolderEntry[] = [];
|
||||
const maxDepth = 15;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
connect-src 'self' * http://* https://* joplin-content://* blob: ;
|
||||
style-src 'unsafe-inline' 'self' blob: joplin-content://* https://* http://* ;
|
||||
child-src 'self' joplin-content://* https://*.youtube.com https://*.youtube-nocookie.com ;
|
||||
script-src 'self' 'unsafe-inline' joplin-content://* ;
|
||||
script-src 'self' joplin-plugin://* joplin-content://* ;
|
||||
media-src 'self' * blob: data: https://* http://* joplin-content://* ;
|
||||
img-src 'self' blob: data: http://* https://* joplin-content://* ;
|
||||
font-src 'self' http://* https://* blob: data: joplin-content://* ;
|
||||
|
||||
@@ -164,13 +164,13 @@ test.describe('pluginApi', () => {
|
||||
await mainScreen.createNewNote('Test note (panels)');
|
||||
|
||||
const panelLocator = await mainScreen.pluginPanelLocator('org.joplinapp.plugins.example.panels');
|
||||
await expect(panelLocator).not.toBeVisible();
|
||||
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
await mainScreen.goToAnything.runCommand(app, 'testShowPanel');
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText('visible');
|
||||
|
||||
// Panel should be visible
|
||||
await expect(panelLocator).toBeVisible();
|
||||
|
||||
// The panel should have the expected content
|
||||
const panelContent = panelLocator.contentFrame();
|
||||
await expect(
|
||||
@@ -178,7 +178,6 @@ test.describe('pluginApi', () => {
|
||||
).toBeAttached();
|
||||
|
||||
await mainScreen.goToAnything.runCommand(app, 'testHidePanel');
|
||||
await expect(noteEditor.codeMirrorEditor).toHaveText('hidden');
|
||||
|
||||
await expect(panelLocator).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
onCommitFiberRoot: function() {},
|
||||
onCommitFiberUnmount: function() {},
|
||||
};
|
||||
|
||||
import './utils/initReact';
|
||||
import './utils/sourceMapSetup';
|
||||
import app from './app';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
|
||||
@@ -151,16 +151,17 @@
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/tools": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.26",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/styled-components": "5.1.36",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "^1.7.7",
|
||||
"codemirror": "5.65.9",
|
||||
@@ -185,17 +186,16 @@
|
||||
"md5": "2.3.0",
|
||||
"moment": "2.30.1",
|
||||
"mustache": "4.2.0",
|
||||
"nan": "2.23.0",
|
||||
"nan": "2.23.1",
|
||||
"node-notifier": "10.0.1",
|
||||
"node-rsa": "1.1.1",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.1.5",
|
||||
"react-dom": "19.1.5",
|
||||
"react-redux": "8.1.3",
|
||||
"react-select": "5.10.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
|
||||
@@ -21,7 +21,7 @@ import Logger from '@joplin/utils/Logger';
|
||||
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import Dialog from '../gui/Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
|
||||
const logger = Logger.create('GotoAnything');
|
||||
@@ -356,13 +356,18 @@ class DialogComponent extends React.PureComponent<Props, State> {
|
||||
results = await Tag.searchAllWithNotes({ titlePattern: searchQuery });
|
||||
} else if (this.state.query.indexOf('@') === 0) { // FOLDERS
|
||||
listType = BaseModel.TYPE_FOLDER;
|
||||
searchQuery = `*${this.state.query.split(' ')[0].substr(1).trim()}*`;
|
||||
results = await Folder.search({ titlePattern: searchQuery });
|
||||
searchQuery = this.state.query.substr(1).trim();
|
||||
const normalizedSearchQuery = removeDiacritics(searchQuery).toLowerCase();
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
results[i] = { ...row, path: path ? path : '/' };
|
||||
results = [];
|
||||
for (const folder of this.props.folders) {
|
||||
if (folder.deleted_time) continue;
|
||||
|
||||
const normalizedTitle = removeDiacritics(folder.title).toLowerCase();
|
||||
if (normalizedSearchQuery && normalizedTitle.indexOf(normalizedSearchQuery) < 0) continue;
|
||||
|
||||
const path = Folder.folderPathString(this.props.folders, folder.parent_id);
|
||||
results.push({ ...folder, path: path ? path : '/' });
|
||||
}
|
||||
} else { // Note TITLE or BODY
|
||||
listType = BaseModel.TYPE_NOTE;
|
||||
|
||||
@@ -122,14 +122,16 @@ function UserWebview(props: Props, ref: any) {
|
||||
} as React.CSSProperties), [contentSize.width, contentSize.height]);
|
||||
|
||||
const src = useMemo(() => {
|
||||
const isolate = Setting.value('featureFlag.plugins.isolatePluginWebViews');
|
||||
let isolate = Setting.value('featureFlag.plugins.isolatePluginWebViews');
|
||||
isolate ||= needsIsolation(props.pluginId);
|
||||
|
||||
const path = toForwardSlashes(getAssetPath('services/plugins/UserWebviewIndex.html'));
|
||||
if (isolate) {
|
||||
return `joplin-content://plugin-webview/${path}`;
|
||||
} else {
|
||||
return `file://${path}`;
|
||||
}
|
||||
}, []);
|
||||
}, [props.pluginId]);
|
||||
|
||||
return <iframe
|
||||
id={props.viewId}
|
||||
@@ -141,3 +143,9 @@ function UserWebview(props: Props, ref: any) {
|
||||
}
|
||||
|
||||
export default forwardRef(UserWebview);
|
||||
|
||||
const needsIsolation = (pluginId: string) => {
|
||||
// Some plugins are broken unless isolated from the main application.
|
||||
// Always enable isolation for these plugins, even if disabled in settings:
|
||||
return ['joplin.plugin.note.tabs', 'joplin.plugin.benji.favorites', 'outline'].includes(pluginId);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||
import UserWebview, { Props as UserWebviewProps } from './UserWebview';
|
||||
import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import Dialog from '../../gui/Dialog';
|
||||
import Dialog from '@joplin/lib/components/Dialog';
|
||||
|
||||
interface Props extends UserWebviewProps {
|
||||
buttons: ButtonSpec[];
|
||||
|
||||
@@ -4,7 +4,7 @@ import bridge from '../../bridge';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function(postMessage: Function, isReady: boolean, scripts: string[], cssFilePath: string) {
|
||||
const protocolHandler = useMemo(() => {
|
||||
return bridge().electronApp().getCustomProtocolHandler();
|
||||
return bridge().electronApp().getContentProtocolHandler();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import useThemeCss from './useThemeCss';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
@@ -8,13 +8,13 @@ describe('useThemeCss', () => {
|
||||
initialProps: { pluginId: 'testid', themeId: Setting.THEME_DARK },
|
||||
});
|
||||
|
||||
await hookResult.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(hookResult.result.current).toContain(`plugin_testid_theme_${Setting.THEME_DARK}.css`);
|
||||
});
|
||||
|
||||
hookResult.rerender({ pluginId: 'testid', themeId: Setting.THEME_LIGHT });
|
||||
|
||||
await hookResult.waitFor(() => {
|
||||
await waitFor(() => {
|
||||
expect(hookResult.result.current).toContain(`plugin_testid_theme_${Setting.THEME_LIGHT}.css`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
TEMP_PATH=~/src/plugin-tests
|
||||
NEED_COMPILING=1
|
||||
PLUGIN_PATH=~/src/plugin-yesyoucan
|
||||
PLUGIN_PATH=~/src/joplin-rich-markdown
|
||||
|
||||
if [[ $NEED_COMPILING == 1 ]]; then
|
||||
mkdir -p "$TEMP_PATH"
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
// Protocol related to note content (e.g. attachments,
|
||||
// the note viewer, etc.)
|
||||
export const contentProtocolName = 'joplin-content';
|
||||
|
||||
// Protocol related to serving plugin content (e.g. editor content
|
||||
// scripts).
|
||||
export const pluginProtocolName = 'joplin-plugin';
|
||||
|
||||
@@ -28,16 +28,23 @@ import { toForwardSlashes } from '@joplin/utils/path';
|
||||
const setUpProtocolHandler = () => {
|
||||
const protocolHandler = handleCustomProtocols();
|
||||
|
||||
expect(handleProtocolMock).toHaveBeenCalledTimes(1);
|
||||
expect(handleProtocolMock).toHaveBeenCalled();
|
||||
|
||||
// Should have registered the protocol.
|
||||
const lastCallArgs = handleProtocolMock.mock.lastCall;
|
||||
expect(lastCallArgs[0]).toBe('joplin-content');
|
||||
let onRequestListener;
|
||||
for (const call of handleProtocolMock.mock.calls) {
|
||||
if (call[0] === 'joplin-content') {
|
||||
// The request listener is the second argument:
|
||||
onRequestListener = call[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the request listener so that it can be called by our tests.
|
||||
const onRequestListener = lastCallArgs[1];
|
||||
// Should have registered the protocol
|
||||
expect(onRequestListener).toBeDefined();
|
||||
|
||||
return { protocolHandler, onRequestListener };
|
||||
return {
|
||||
appProtocolHandler: protocolHandler.appContent,
|
||||
onRequestListener,
|
||||
};
|
||||
};
|
||||
|
||||
interface ExpectBlockedOptions {
|
||||
@@ -67,7 +74,7 @@ const expectPathToBeUnblocked = async (onRequestListener: ProtocolHandler, fileP
|
||||
};
|
||||
|
||||
|
||||
describe('handleCustomProtocols', () => {
|
||||
describe('handleCustomProtocols.content', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks between tests to ensure a clean testing environment.
|
||||
customProtocols.clear();
|
||||
@@ -76,7 +83,7 @@ describe('handleCustomProtocols', () => {
|
||||
});
|
||||
|
||||
test('should only allow access to files in allowed directories', async () => {
|
||||
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
|
||||
await expectPathToBeBlocked(onRequestListener, '/test/path');
|
||||
await expectPathToBeBlocked(onRequestListener, '/');
|
||||
@@ -98,7 +105,7 @@ describe('handleCustomProtocols', () => {
|
||||
});
|
||||
|
||||
test('should be possible to allow and remove read access for a file', async () => {
|
||||
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
await expectPathToBeBlocked(onRequestListener, '/test/path/a.txt');
|
||||
|
||||
const handle1 = protocolHandler.allowReadAccessToFile('/test/path/a.txt');
|
||||
@@ -113,7 +120,7 @@ describe('handleCustomProtocols', () => {
|
||||
});
|
||||
|
||||
test('should only allow access to file-media/ URLs when given the correct access key', async () => {
|
||||
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
const expectBlocked = (path: string) => {
|
||||
return expectPathToBeBlocked(onRequestListener, path, { host: 'file-media' });
|
||||
};
|
||||
@@ -141,7 +148,7 @@ describe('handleCustomProtocols', () => {
|
||||
});
|
||||
|
||||
test('should allow requesting part of a file', async () => {
|
||||
const { protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
const { appProtocolHandler: protocolHandler, onRequestListener } = setUpProtocolHandler();
|
||||
|
||||
protocolHandler.allowReadAccessToDirectory(`${supportDir}/`);
|
||||
const targetFilePath = join(supportDir, 'photo.jpg');
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user