mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-17 18:44:45 +02:00
parent
4322acc5b7
commit
6879481fd5
@ -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.
|
Loading…
Reference in New Issue
Block a user