You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	| @@ -693,9 +693,6 @@ packages/app-desktop/services/commands/stateToWhenClauseContext.js.map | ||||
| packages/app-desktop/services/commands/types.d.ts | ||||
| packages/app-desktop/services/commands/types.js | ||||
| packages/app-desktop/services/commands/types.js.map | ||||
| packages/app-desktop/services/e2ee.d.ts | ||||
| packages/app-desktop/services/e2ee.js | ||||
| packages/app-desktop/services/e2ee.js.map | ||||
| packages/app-desktop/services/plugins/PlatformImplementation.d.ts | ||||
| packages/app-desktop/services/plugins/PlatformImplementation.js | ||||
| packages/app-desktop/services/plugins/PlatformImplementation.js.map | ||||
| @@ -945,6 +942,12 @@ packages/lib/TaskQueue.js.map | ||||
| packages/lib/array.d.ts | ||||
| packages/lib/array.js | ||||
| packages/lib/array.js.map | ||||
| packages/lib/callbackUrlUtils.d.ts | ||||
| packages/lib/callbackUrlUtils.js | ||||
| packages/lib/callbackUrlUtils.js.map | ||||
| packages/lib/callbackUrlUtils.test.d.ts | ||||
| packages/lib/callbackUrlUtils.test.js | ||||
| packages/lib/callbackUrlUtils.test.js.map | ||||
| packages/lib/commands/historyBackward.d.ts | ||||
| packages/lib/commands/historyBackward.js | ||||
| packages/lib/commands/historyBackward.js.map | ||||
|   | ||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -676,9 +676,6 @@ packages/app-desktop/services/commands/stateToWhenClauseContext.js.map | ||||
| packages/app-desktop/services/commands/types.d.ts | ||||
| packages/app-desktop/services/commands/types.js | ||||
| packages/app-desktop/services/commands/types.js.map | ||||
| packages/app-desktop/services/e2ee.d.ts | ||||
| packages/app-desktop/services/e2ee.js | ||||
| packages/app-desktop/services/e2ee.js.map | ||||
| packages/app-desktop/services/plugins/PlatformImplementation.d.ts | ||||
| packages/app-desktop/services/plugins/PlatformImplementation.js | ||||
| packages/app-desktop/services/plugins/PlatformImplementation.js.map | ||||
| @@ -928,6 +925,12 @@ packages/lib/TaskQueue.js.map | ||||
| packages/lib/array.d.ts | ||||
| packages/lib/array.js | ||||
| packages/lib/array.js.map | ||||
| packages/lib/callbackUrlUtils.d.ts | ||||
| packages/lib/callbackUrlUtils.js | ||||
| packages/lib/callbackUrlUtils.js.map | ||||
| packages/lib/callbackUrlUtils.test.d.ts | ||||
| packages/lib/callbackUrlUtils.test.js | ||||
| packages/lib/callbackUrlUtils.test.js.map | ||||
| packages/lib/commands/historyBackward.d.ts | ||||
| packages/lib/commands/historyBackward.js | ||||
| packages/lib/commands/historyBackward.js.map | ||||
|   | ||||
| @@ -187,7 +187,7 @@ if command -v lsb_release &> /dev/null; then | ||||
|   # Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling. | ||||
|   if [[ $DISTVER =~ Debian1. ]] || [ "$DISTVER" = "Linuxmint4" ] && [ "$DISTCODENAME" = "debbie" ] || [ "$DISTVER" = "CentOS" ] && [[ "$DISTMAJOR" =~ 6|7 ]] | ||||
|   then | ||||
|     SANDBOXPARAM=" --no-sandbox" | ||||
|     SANDBOXPARAM="--no-sandbox" | ||||
|   fi | ||||
| fi | ||||
|  | ||||
| @@ -206,7 +206,21 @@ then | ||||
|  | ||||
|     # On some systems this directory doesn't exist by default | ||||
|     mkdir -p ~/.local/share/applications | ||||
|     echo -e "[Desktop Entry]\nEncoding=UTF-8\nName=Joplin\nComment=Joplin for Desktop\nExec=${HOME}/.joplin/Joplin.AppImage${SANDBOXPARAM}\nIcon=joplin\nStartupWMClass=Joplin\nType=Application\nCategories=Office;" >> ~/.local/share/applications/appimagekit-joplin.desktop | ||||
|      | ||||
|     # Tabs specifically, and not spaces, are needed for indentation with Bash heredocs | ||||
|     cat >> ~/.local/share/applications/appimagekit-joplin.desktop <<-EOF | ||||
| 	[Desktop Entry] | ||||
| 	Encoding=UTF-8 | ||||
| 	Name=Joplin | ||||
| 	Comment=Joplin for Desktop | ||||
| 	Exec=${HOME}/.joplin/Joplin.AppImage ${SANDBOXPARAM} | ||||
| 	Icon=joplin | ||||
| 	StartupWMClass=Joplin | ||||
| 	Type=Application | ||||
| 	Categories=Office; | ||||
| 	MimeType=x-scheme-handler/joplin; | ||||
| 	EOF | ||||
|      | ||||
|     # Update application icons | ||||
|     [[ `command -v update-desktop-database` ]] && update-desktop-database ~/.local/share/applications && update-desktop-database ~/.local/share/icons | ||||
|     print "${COLOR_GREEN}OK${COLOR_RESET}" | ||||
|   | ||||
| @@ -98,6 +98,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr | ||||
| 	- [What is a conflict?](https://github.com/laurent22/joplin/blob/dev/readme/conflict.md) | ||||
| 	- [How to enable debug mode](https://github.com/laurent22/joplin/blob/dev/readme/debugging.md) | ||||
| 	- [About the Rich Text editor limitations](https://github.com/laurent22/joplin/blob/dev/readme/rich_text_editor.md) | ||||
| 	- [External links](https://github.com/laurent22/joplin/blob/dev/readme/external_links.md) | ||||
| 	- [FAQ](https://github.com/laurent22/joplin/blob/dev/readme/faq.md) | ||||
|  | ||||
| - Joplin Cloud | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import Logger from '@joplin/lib/Logger'; | ||||
| import { PluginMessage } from './services/plugins/PluginRunner'; | ||||
| import shim from '@joplin/lib/shim'; | ||||
| import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | ||||
|  | ||||
| const { BrowserWindow, Tray, screen } = require('electron'); | ||||
| const url = require('url'); | ||||
| @@ -30,12 +31,14 @@ export default class ElectronAppWrapper { | ||||
| 	private buildDir_: string = null; | ||||
| 	private rendererProcessQuitReply_: RendererProcessQuitReply = null; | ||||
| 	private pluginWindows_: PluginWindows = {}; | ||||
| 	private initialCallbackUrl_: string = null; | ||||
|  | ||||
| 	constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean) { | ||||
| 	constructor(electronApp: any, env: string, profilePath: string, isDebugMode: boolean, initialCallbackUrl: string) { | ||||
| 		this.electronApp_ = electronApp; | ||||
| 		this.env_ = env; | ||||
| 		this.isDebugMode_ = isDebugMode; | ||||
| 		this.profilePath_ = profilePath; | ||||
| 		this.initialCallbackUrl_ = initialCallbackUrl; | ||||
| 	} | ||||
|  | ||||
| 	electronApp() { | ||||
| @@ -58,6 +61,10 @@ export default class ElectronAppWrapper { | ||||
| 		return this.env_; | ||||
| 	} | ||||
|  | ||||
| 	initialCallbackUrl() { | ||||
| 		return this.initialCallbackUrl_; | ||||
| 	} | ||||
|  | ||||
| 	createWindow() { | ||||
| 		// Set to true to view errors if the application does not start | ||||
| 		const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_; | ||||
| @@ -236,7 +243,7 @@ export default class ElectronAppWrapper { | ||||
| 	async waitForElectronAppReady() { | ||||
| 		if (this.electronApp().isReady()) return Promise.resolve(); | ||||
|  | ||||
| 		return new Promise((resolve) => { | ||||
| 		return new Promise<void>((resolve) => { | ||||
| 			const iid = setInterval(() => { | ||||
| 				if (this.electronApp().isReady()) { | ||||
| 					clearInterval(iid); | ||||
| @@ -323,12 +330,18 @@ export default class ElectronAppWrapper { | ||||
| 		} | ||||
|  | ||||
| 		// Someone tried to open a second instance - focus our window instead | ||||
| 		this.electronApp_.on('second-instance', () => { | ||||
| 		this.electronApp_.on('second-instance', (_e: any, argv: string[]) => { | ||||
| 			const win = this.window(); | ||||
| 			if (!win) return; | ||||
| 			if (win.isMinimized()) win.restore(); | ||||
| 			win.show(); | ||||
| 			win.focus(); | ||||
| 			if (process.platform !== 'darwin') { | ||||
| 				const url = argv.find((arg) => isCallbackUrl(arg)); | ||||
| 				if (url) { | ||||
| 					void this.openCallbackUrl(url); | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		return false; | ||||
| @@ -355,6 +368,17 @@ export default class ElectronAppWrapper { | ||||
| 		this.electronApp_.on('activate', () => { | ||||
| 			this.win_.show(); | ||||
| 		}); | ||||
|  | ||||
| 		this.electronApp_.on('open-url', (event: any, url: string) => { | ||||
| 			event.preventDefault(); | ||||
| 			void this.openCallbackUrl(url); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	async openCallbackUrl(url: string) { | ||||
| 		this.win_.webContents.send('asynchronous-message', 'openCallbackUrl', { | ||||
| 			url: url, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -34,6 +34,8 @@ import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog'; | ||||
| import { ShareInvitation } from '@joplin/lib/services/share/reducer'; | ||||
| import removeKeylessItems from '../ResizableLayout/utils/removeKeylessItems'; | ||||
| import { localSyncInfoFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils'; | ||||
| import { parseCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | ||||
| import ElectronAppWrapper from '../../ElectronAppWrapper'; | ||||
| import { showMissingMasterKeyMessage } from '@joplin/lib/services/e2ee/utils'; | ||||
| import commands from './commands/index'; | ||||
| import invitationRespond from '../../services/share/invitationRespond'; | ||||
| @@ -154,6 +156,23 @@ class MainScreenComponent extends React.Component<Props, State> { | ||||
| 		this.layoutModeListenerKeyDown = this.layoutModeListenerKeyDown.bind(this); | ||||
|  | ||||
| 		window.addEventListener('resize', this.window_resize); | ||||
|  | ||||
| 		ipcRenderer.on('asynchronous-message', (_event: any, message: string, args: any) => { | ||||
| 			if (message === 'openCallbackUrl') { | ||||
| 				this.openCallbackUrl(args.url); | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		const initialCallbackUrl = (bridge().electronApp() as ElectronAppWrapper).initialCallbackUrl(); | ||||
| 		if (initialCallbackUrl) { | ||||
| 			this.openCallbackUrl(initialCallbackUrl); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private openCallbackUrl(url: string) { | ||||
| 		console.log(`openUrl ${url}`); | ||||
| 		const { command, params } = parseCallbackUrl(url); | ||||
| 		void CommandService.instance().execute(command.toString(), params.id); | ||||
| 	} | ||||
|  | ||||
| 	private updateLayoutPluginViews(layout: LayoutItem, plugins: PluginStates) { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import Logger from '@joplin/lib/Logger'; | ||||
| import { FolderEntity } from '@joplin/lib/services/database/types'; | ||||
| import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext'; | ||||
| import { store } from '@joplin/lib/reducer'; | ||||
| import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | ||||
| const { connect } = require('react-redux'); | ||||
| const shared = require('@joplin/lib/components/shared/side-menu-shared.js'); | ||||
| const { themeStyle } = require('@joplin/lib/theme'); | ||||
| @@ -28,6 +29,7 @@ const Menu = bridge().Menu; | ||||
| const MenuItem = bridge().MenuItem; | ||||
| const { substrWithEllipsis } = require('@joplin/lib/string-utils'); | ||||
| const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids'); | ||||
| const { clipboard } = require('electron'); | ||||
|  | ||||
| const logger = Logger.create('Sidebar'); | ||||
|  | ||||
| @@ -332,10 +334,29 @@ class SidebarComponent extends React.Component<Props, State> { | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		if (itemType === BaseModel.TYPE_FOLDER) { | ||||
| 			menu.append( | ||||
| 				new MenuItem({ | ||||
| 					label: _('Copy external link'), | ||||
| 					click: () => { | ||||
| 						clipboard.writeText(getFolderCallbackUrl(itemId)); | ||||
| 					}, | ||||
| 				}) | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		if (itemType === BaseModel.TYPE_TAG) { | ||||
| 			menu.append(new MenuItem( | ||||
| 				menuUtils.commandToStatefulMenuItem('renameTag', itemId) | ||||
| 			)); | ||||
| 			menu.append( | ||||
| 				new MenuItem({ | ||||
| 					label: _('Copy external link'), | ||||
| 					click: () => { | ||||
| 						clipboard.writeText(getTagCallbackUrl(itemId)); | ||||
| 					}, | ||||
| 				}) | ||||
| 			); | ||||
| 		} | ||||
|  | ||||
| 		const pluginViews = pluginUtils.viewsByType(this.pluginsRef.current, 'menuItem'); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; | ||||
| import InteropServiceHelper from '../../InteropServiceHelper'; | ||||
| import { _ } from '@joplin/lib/locale'; | ||||
| import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types'; | ||||
| import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils'; | ||||
|  | ||||
| import BaseModel from '@joplin/lib/BaseModel'; | ||||
| const bridge = require('@electron/remote').require('./bridge').default; | ||||
| @@ -13,6 +14,7 @@ const Menu = bridge().Menu; | ||||
| const MenuItem = bridge().MenuItem; | ||||
| import Note from '@joplin/lib/models/Note'; | ||||
| import Setting from '@joplin/lib/models/Setting'; | ||||
| const { clipboard } = require('electron'); | ||||
|  | ||||
| interface ContextMenuProps { | ||||
| 	notes: any[]; | ||||
| @@ -121,7 +123,6 @@ export default class NoteListUtils { | ||||
| 				new MenuItem({ | ||||
| 					label: _('Copy Markdown link'), | ||||
| 					click: async () => { | ||||
| 						const { clipboard } = require('electron'); | ||||
| 						const links = []; | ||||
| 						for (let i = 0; i < noteIds.length; i++) { | ||||
| 							const note = await Note.load(noteIds[i]); | ||||
| @@ -132,6 +133,17 @@ export default class NoteListUtils { | ||||
| 				}) | ||||
| 			); | ||||
|  | ||||
| 			if (noteIds.length === 1) { | ||||
| 				menu.append( | ||||
| 					new MenuItem({ | ||||
| 						label: _('Copy external link'), | ||||
| 						click: () => { | ||||
| 							clipboard.writeText(getNoteCallbackUrl(noteIds[0])); | ||||
| 						}, | ||||
| 					}) | ||||
| 				); | ||||
| 			} | ||||
|  | ||||
| 			if ([9, 10].includes(Setting.value('sync.target'))) { | ||||
| 				menu.append( | ||||
| 					new MenuItem( | ||||
|   | ||||
| @@ -8,6 +8,7 @@ const Logger = require('@joplin/lib/Logger').default; | ||||
| const FsDriverNode = require('@joplin/lib/fs-driver-node').default; | ||||
| const envFromArgs = require('@joplin/lib/envFromArgs'); | ||||
| const packageInfo = require('./packageInfo.js'); | ||||
| const { isCallbackUrl } = require('@joplin/lib/callbackUrlUtils'); | ||||
|  | ||||
| // Electron takes the application name from package.json `name` and | ||||
| // displays this in the tray icon toolip and message box titles, however in | ||||
| @@ -37,7 +38,11 @@ const env = envFromArgs(process.argv); | ||||
| const profilePath = profileFromArgs(process.argv); | ||||
| const isDebugMode = !!process.argv && process.argv.indexOf('--debug') >= 0; | ||||
|  | ||||
| const wrapper = new ElectronAppWrapper(electronApp, env, profilePath, isDebugMode); | ||||
| electronApp.setAsDefaultProtocolClient('joplin'); | ||||
|  | ||||
| const initialCallbackUrl = process.argv.find((arg) => isCallbackUrl(arg)); | ||||
|  | ||||
| const wrapper = new ElectronAppWrapper(electronApp, env, profilePath, isDebugMode, initialCallbackUrl); | ||||
|  | ||||
| initBridge(wrapper); | ||||
|  | ||||
|   | ||||
| @@ -77,13 +77,23 @@ | ||||
|       "icon": "../../Assets/macOs.icns", | ||||
|       "target": "dmg", | ||||
|       "hardenedRuntime": true, | ||||
|       "entitlements": "./build-mac/entitlements.mac.inherit.plist" | ||||
|       "entitlements": "./build-mac/entitlements.mac.inherit.plist", | ||||
|       "extendInfo": { | ||||
|         "CFBundleURLTypes": [ | ||||
|           { | ||||
|             "CFBundleURLSchemes": ["joplin"], | ||||
|             "CFBundleTypeRole": "Editor", | ||||
|             "CFBundleURLName": "org.joplinapp.x-callback-url" | ||||
|           } | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "linux": { | ||||
|       "icon": "../../Assets/LinuxIcons", | ||||
|       "category": "Office", | ||||
|       "desktop": { | ||||
|         "Icon": "joplin" | ||||
|         "Icon": "joplin", | ||||
|         "MimeType": "x-scheme-handler/joplin;" | ||||
|       }, | ||||
|       "target": "AppImage" | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										64
									
								
								packages/lib/callbackUrlUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								packages/lib/callbackUrlUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import * as callbackUrlUtils from './callbackUrlUtils'; | ||||
|  | ||||
| describe('callbackUrlUtils', function() { | ||||
|  | ||||
| 	it('should identify valid callback urls', () => { | ||||
| 		const url = 'joplin://x-callback-url/123?a=b'; | ||||
| 		expect(callbackUrlUtils.isCallbackUrl(url)).toBe(true); | ||||
| 	}); | ||||
|  | ||||
| 	it('should identify invalid callback urls', () => { | ||||
| 		expect(callbackUrlUtils.isCallbackUrl('not-joplin://x-callback-url/123?a=b')).toBe(false); | ||||
| 		expect(callbackUrlUtils.isCallbackUrl('joplin://xcallbackurl/123?a=b')).toBe(false); | ||||
| 	}); | ||||
|  | ||||
| 	it('should build valid note callback urls', () => { | ||||
| 		const noteUrl = callbackUrlUtils.getNoteCallbackUrl('123456'); | ||||
| 		expect(callbackUrlUtils.isCallbackUrl(noteUrl)).toBe(true); | ||||
| 		expect(noteUrl).toBe('joplin://x-callback-url/openNote?id=123456'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should build valid folder callback urls', () => { | ||||
| 		const folderUrl = callbackUrlUtils.getFolderCallbackUrl('123456'); | ||||
| 		expect(callbackUrlUtils.isCallbackUrl(folderUrl)).toBe(true); | ||||
| 		expect(folderUrl).toBe('joplin://x-callback-url/openFolder?id=123456'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should build valid tag callback urls', () => { | ||||
| 		const tagUrl = callbackUrlUtils.getTagCallbackUrl('123456'); | ||||
| 		expect(callbackUrlUtils.isCallbackUrl(tagUrl)).toBe(true); | ||||
| 		expect(tagUrl).toBe('joplin://x-callback-url/openTag?id=123456'); | ||||
| 	}); | ||||
|  | ||||
| 	it('should parse note callback urls', () => { | ||||
| 		const parsed = callbackUrlUtils.parseCallbackUrl('joplin://x-callback-url/openNote?id=123456'); | ||||
| 		expect(parsed.command).toBe(callbackUrlUtils.CallbackUrlCommand.OpenNote); | ||||
| 		expect(parsed.params).toStrictEqual({ id: '123456' }); | ||||
| 	}); | ||||
|  | ||||
| 	it('should parse folder callback urls', () => { | ||||
| 		const parsed = callbackUrlUtils.parseCallbackUrl('joplin://x-callback-url/openFolder?id=123456'); | ||||
| 		expect(parsed.command).toBe(callbackUrlUtils.CallbackUrlCommand.OpenFolder); | ||||
| 		expect(parsed.params).toStrictEqual({ id: '123456' }); | ||||
| 	}); | ||||
|  | ||||
| 	it('should parse tag callback urls', () => { | ||||
| 		const parsed = callbackUrlUtils.parseCallbackUrl('joplin://x-callback-url/openTag?id=123456'); | ||||
| 		expect(parsed.command).toBe(callbackUrlUtils.CallbackUrlCommand.OpenTag); | ||||
| 		expect(parsed.params).toStrictEqual({ id: '123456' }); | ||||
| 	}); | ||||
|  | ||||
| 	it('should throw an error on invalid input', () => { | ||||
| 		expect(() => callbackUrlUtils.parseCallbackUrl('not-a-url')) | ||||
| 			.toThrowError('Invalid callback url not-a-url'); | ||||
|  | ||||
| 		expect(() => callbackUrlUtils.parseCallbackUrl('not-joplin://x-callback-url/123?a=b')) | ||||
| 			.toThrowError('Invalid callback url not-joplin://x-callback-url/123?a=b'); | ||||
|  | ||||
| 		expect(() => callbackUrlUtils.parseCallbackUrl('joplin://xcallbackurl/123?a=b')) | ||||
| 			.toThrowError('Invalid callback url joplin://xcallbackurl/123?a=b'); | ||||
| 	}); | ||||
|  | ||||
|  | ||||
|  | ||||
| }); | ||||
							
								
								
									
										37
									
								
								packages/lib/callbackUrlUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								packages/lib/callbackUrlUtils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| const URL = require('url-parse'); | ||||
|  | ||||
| export function isCallbackUrl(s: string) { | ||||
| 	return s.startsWith('joplin://x-callback-url/'); | ||||
| } | ||||
|  | ||||
| export function getNoteCallbackUrl(noteId: string) { | ||||
| 	return `joplin://x-callback-url/openNote?id=${encodeURIComponent(noteId)}`; | ||||
| } | ||||
|  | ||||
| export function getFolderCallbackUrl(folderId: string) { | ||||
| 	return `joplin://x-callback-url/openFolder?id=${encodeURIComponent(folderId)}`; | ||||
| } | ||||
|  | ||||
| export function getTagCallbackUrl(tagId: string) { | ||||
| 	return `joplin://x-callback-url/openTag?id=${encodeURIComponent(tagId)}`; | ||||
| } | ||||
|  | ||||
| export const enum CallbackUrlCommand { | ||||
|     OpenNote = 'openNote', | ||||
|     OpenFolder = 'openFolder', | ||||
|     OpenTag = 'openTag', | ||||
| } | ||||
|  | ||||
| export interface CallbackUrlInfo { | ||||
|     command: CallbackUrlCommand; | ||||
|     params: Record<string, string>; | ||||
| } | ||||
|  | ||||
| export function parseCallbackUrl(s: string): CallbackUrlInfo { | ||||
| 	if (!isCallbackUrl(s)) throw new Error(`Invalid callback url ${s}`); | ||||
| 	const url = new URL(s, true); | ||||
| 	return { | ||||
| 		command: url.pathname.substring(url.pathname.lastIndexOf('/') + 1) as CallbackUrlCommand, | ||||
| 		params: url.query, | ||||
| 	}; | ||||
| } | ||||
							
								
								
									
										15
									
								
								readme/external_links.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								readme/external_links.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| # External URL links | ||||
|  | ||||
| This feature allows creation of links to notes, folder, and tags. When opening such link Joplin will start, unless it's already running, and open the corresponding item. | ||||
|  | ||||
| To create a link, right click a note, a folder, or a tag in the sidebar and select "Copy external link". The link will be copied to clipboard. | ||||
|  | ||||
| ## Link format | ||||
|  | ||||
| * `joplin://x-callback-url/openNote?id=<note id>` for note | ||||
| * `joplin://x-callback-url/openFolder?id=<folder id>` for folder | ||||
| * `joplin://x-callback-url/openTag?id=<tag id>` for tag | ||||
|  | ||||
| ## Known problems | ||||
|  | ||||
| On macOS if Joplin isn't running it will start but it won't open the note. | ||||
		Reference in New Issue
	
	Block a user