mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
refactor: remove bookmark plugin (#4544)
This commit is contained in:
244
apps/core/src/components/bookmark.tsx
Normal file
244
apps/core/src/components/bookmark.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { MenuItem, PureMenu } from '@affine/component';
|
||||
import { MuiClickAwayListener } from '@affine/component';
|
||||
import type { SerializedBlock } from '@blocksuite/blocks';
|
||||
import type { BaseBlockModel } from '@blocksuite/store';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { VEditor } from '@blocksuite/virgo';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type ShortcutMap = {
|
||||
[key: string]: (e: KeyboardEvent, page: Page) => void;
|
||||
};
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
id: 'dismiss',
|
||||
label: 'Dismiss',
|
||||
},
|
||||
{
|
||||
id: 'bookmark',
|
||||
label: 'Create bookmark',
|
||||
},
|
||||
];
|
||||
|
||||
function getCurrentNativeRange(selection = window.getSelection()) {
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
if (selection.rangeCount === 0) {
|
||||
return null;
|
||||
}
|
||||
if (selection.rangeCount > 1) {
|
||||
console.warn('getCurrentRange may be wrong, rangeCount > 1');
|
||||
}
|
||||
return selection.getRangeAt(0);
|
||||
}
|
||||
|
||||
const handleEnter = ({
|
||||
page,
|
||||
selectedOption,
|
||||
callback,
|
||||
}: {
|
||||
page: Page;
|
||||
selectedOption: keyof ShortcutMap;
|
||||
callback: () => void;
|
||||
}) => {
|
||||
if (selectedOption === 'dismiss') {
|
||||
return callback();
|
||||
}
|
||||
const native = getCurrentNativeRange();
|
||||
if (!native) {
|
||||
return callback();
|
||||
}
|
||||
const container = native.startContainer;
|
||||
const element =
|
||||
container instanceof Element ? container : container?.parentElement;
|
||||
const virgo = element?.closest<Element & { virgoEditor: VEditor }>(
|
||||
'[data-virgo-root]'
|
||||
)?.virgoEditor;
|
||||
if (!virgo) {
|
||||
return callback();
|
||||
}
|
||||
const linkInfo = virgo
|
||||
?.getDeltasByVRange({
|
||||
index: native.startOffset,
|
||||
length: 0,
|
||||
})
|
||||
.find(delta => delta[0]?.attributes?.link);
|
||||
if (!linkInfo) {
|
||||
return;
|
||||
}
|
||||
const [, { index, length }] = linkInfo;
|
||||
const link = linkInfo[0]?.attributes?.link as string;
|
||||
|
||||
const model = element?.closest<Element & { model: BaseBlockModel }>(
|
||||
'[data-block-id]'
|
||||
)?.model;
|
||||
if (!model) {
|
||||
return callback();
|
||||
}
|
||||
const parent = page.getParent(model);
|
||||
if (!parent) {
|
||||
return callback();
|
||||
}
|
||||
const currentBlockIndex = parent.children.indexOf(model);
|
||||
page.addBlock(
|
||||
'affine:bookmark',
|
||||
{ url: link },
|
||||
parent,
|
||||
currentBlockIndex + 1
|
||||
);
|
||||
|
||||
virgo?.deleteText({
|
||||
index,
|
||||
length,
|
||||
});
|
||||
|
||||
if (model.isEmpty()) {
|
||||
page.deleteBlock(model);
|
||||
}
|
||||
return callback();
|
||||
};
|
||||
|
||||
const shouldShowBookmarkMenu = (pastedBlocks: Record<string, unknown>[]) => {
|
||||
if (!pastedBlocks.length || pastedBlocks.length > 1) {
|
||||
return;
|
||||
}
|
||||
const [firstBlock] = pastedBlocks as [SerializedBlock];
|
||||
if (
|
||||
!firstBlock.text ||
|
||||
!firstBlock.text.length ||
|
||||
firstBlock.text.length > 1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return !!firstBlock.text[0].attributes?.link;
|
||||
};
|
||||
|
||||
export type BookmarkProps = {
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export const Bookmark = ({ page }: BookmarkProps) => {
|
||||
const [anchor, setAnchor] = useState<Range | null>(null);
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
menuOptions[0].id
|
||||
);
|
||||
const shortcutMap = useMemo<ShortcutMap>(
|
||||
() => ({
|
||||
ArrowUp: () => {
|
||||
const curIndex = menuOptions.findIndex(
|
||||
({ id }) => id === selectedOption
|
||||
);
|
||||
if (menuOptions[curIndex - 1]) {
|
||||
setSelectedOption(menuOptions[curIndex - 1].id);
|
||||
} else if (curIndex === -1) {
|
||||
setSelectedOption(menuOptions[0].id);
|
||||
} else {
|
||||
setSelectedOption(menuOptions[menuOptions.length - 1].id);
|
||||
}
|
||||
},
|
||||
ArrowDown: () => {
|
||||
const curIndex = menuOptions.findIndex(
|
||||
({ id }) => id === selectedOption
|
||||
);
|
||||
if (curIndex !== -1 && menuOptions[curIndex + 1]) {
|
||||
setSelectedOption(menuOptions[curIndex + 1].id);
|
||||
} else {
|
||||
setSelectedOption(menuOptions[0].id);
|
||||
}
|
||||
},
|
||||
Enter: () =>
|
||||
handleEnter({
|
||||
page,
|
||||
selectedOption,
|
||||
callback: () => {
|
||||
setAnchor(null);
|
||||
},
|
||||
}),
|
||||
Escape: () => {
|
||||
setAnchor(null);
|
||||
},
|
||||
}),
|
||||
[page, selectedOption]
|
||||
);
|
||||
const onKeydown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const shortcut = shortcutMap[e.key];
|
||||
if (shortcut) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
shortcut(e, page);
|
||||
} else {
|
||||
setAnchor(null);
|
||||
}
|
||||
},
|
||||
[page, shortcutMap]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposer = page.slots.pasted.on(pastedBlocks => {
|
||||
if (!shouldShowBookmarkMenu(pastedBlocks)) {
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
setAnchor(getCurrentNativeRange());
|
||||
}, 100);
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposer.dispose();
|
||||
};
|
||||
}, [onKeydown, page, shortcutMap]);
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor) {
|
||||
document.addEventListener('keydown', onKeydown, { capture: true });
|
||||
} else {
|
||||
// reset status and remove event
|
||||
setSelectedOption(menuOptions[0].id);
|
||||
document.removeEventListener('keydown', onKeydown, { capture: true });
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeydown, { capture: true });
|
||||
};
|
||||
}, [anchor, onKeydown]);
|
||||
|
||||
return anchor ? (
|
||||
<MuiClickAwayListener
|
||||
onClickAway={() => {
|
||||
setAnchor(null);
|
||||
setSelectedOption('');
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<PureMenu open={!!anchor} anchorEl={anchor} placement="bottom-start">
|
||||
{menuOptions.map(({ id, label }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={id}
|
||||
active={selectedOption === id}
|
||||
onClick={() => {
|
||||
handleEnter({
|
||||
page,
|
||||
selectedOption: id,
|
||||
callback: () => {
|
||||
setAnchor(null);
|
||||
},
|
||||
});
|
||||
}}
|
||||
disableHover={true}
|
||||
onMouseEnter={() => {
|
||||
setSelectedOption(id);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</PureMenu>
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
) : null;
|
||||
};
|
||||
@@ -31,6 +31,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import { Bookmark } from './bookmark';
|
||||
import * as styles from './page-detail-editor.css';
|
||||
import { editorContainer, pluginContainer } from './page-detail-editor.css';
|
||||
import { TrashButtonGroup } from './pure/trash-button-group';
|
||||
@@ -139,6 +140,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
)}
|
||||
/>
|
||||
{meta.trash && <TrashButtonGroup />}
|
||||
<Bookmark page={page} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"main": "./dist/main.js",
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/bookmark-plugin": "workspace:*",
|
||||
"@affine/copilot-plugin": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/hello-world-plugin": "workspace:*",
|
||||
|
||||
@@ -32,7 +32,6 @@ export const config = () => {
|
||||
resolve(electronDir, './src/main/index.ts'),
|
||||
resolve(electronDir, './src/preload/index.ts'),
|
||||
resolve(electronDir, './src/helper/index.ts'),
|
||||
resolve(electronDir, './src/worker/plugin.ts'),
|
||||
],
|
||||
entryNames: '[dir]',
|
||||
outdir: resolve(electronDir, './dist'),
|
||||
|
||||
@@ -64,7 +64,6 @@ export const registerHandlers = () => {
|
||||
ipcMain.handle(chan, async (e, ...args) => {
|
||||
const start = performance.now();
|
||||
try {
|
||||
// @ts-expect-error - TODO: fix this
|
||||
const result = await handler(e, ...args);
|
||||
logger.info(
|
||||
'[ipc-api]',
|
||||
|
||||
@@ -9,7 +9,6 @@ import { registerHandlers } from './handlers';
|
||||
import { ensureHelperProcess } from './helper-process';
|
||||
import { logger } from './logger';
|
||||
import { restoreOrCreateWindow } from './main-window';
|
||||
import { registerPlugin } from './plugin';
|
||||
import { registerProtocol } from './protocol';
|
||||
import { registerUpdater } from './updater';
|
||||
|
||||
@@ -59,7 +58,6 @@ setupDeepLink(app);
|
||||
app
|
||||
.whenReady()
|
||||
.then(registerProtocol)
|
||||
.then(registerPlugin)
|
||||
.then(registerHandlers)
|
||||
.then(registerEvents)
|
||||
.then(ensureHelperProcess)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { join, resolve } from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import { logger, pluginLogger } from '@affine/electron/main/logger';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
import { ipcMain } from 'electron';
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
|
||||
const builtInPlugins = ['bookmark'];
|
||||
|
||||
declare global {
|
||||
// fixme(himself65):
|
||||
// remove this when bookmark block plugin is migrated to plugin-infra
|
||||
// eslint-disable-next-line no-var
|
||||
var asyncCall: Record<string, (...args: any) => PromiseLike<any>>;
|
||||
}
|
||||
|
||||
export async function registerPlugin() {
|
||||
logger.info('import plugin manager');
|
||||
const asyncCall = AsyncCall<
|
||||
Record<string, (...args: any) => PromiseLike<any>>
|
||||
>(
|
||||
{
|
||||
log: (...args: any[]) => {
|
||||
pluginLogger.log(...args);
|
||||
},
|
||||
},
|
||||
{
|
||||
channel: new MessageEventChannel(
|
||||
new Worker(resolve(__dirname, './worker.js'), {})
|
||||
),
|
||||
}
|
||||
);
|
||||
globalThis.asyncCall = asyncCall;
|
||||
await Promise.all(
|
||||
builtInPlugins.map(async plugin => {
|
||||
const pluginPackageJsonPath = join(
|
||||
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
|
||||
`./${plugin}/package.json`
|
||||
);
|
||||
logger.info(`${plugin} plugin path:`, pluginPackageJsonPath);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(pluginPackageJsonPath, 'utf-8')
|
||||
);
|
||||
console.log('packageJson', packageJson);
|
||||
const serverCommand: string[] = packageJson.affinePlugin.serverCommand;
|
||||
serverCommand.forEach(command => {
|
||||
ipcMain.handle(command, async (_, ...args) => {
|
||||
logger.info(`plugin ${plugin} called`);
|
||||
return asyncCall[command](...args);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app, BrowserWindow, nativeTheme } from 'electron';
|
||||
import { getLinkPreview } from 'link-preview-js';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
@@ -43,12 +44,30 @@ export const uiHandlers = {
|
||||
getGoogleOauthCode: async () => {
|
||||
return getGoogleOauthCode();
|
||||
},
|
||||
/**
|
||||
* @deprecated Remove this when bookmark block plugin is migrated to plugin-infra
|
||||
*/
|
||||
getBookmarkDataByLink: async (_, link: string) => {
|
||||
return globalThis.asyncCall[
|
||||
'com.blocksuite.bookmark-block.get-bookmark-data-by-link'
|
||||
](link);
|
||||
const previewData = (await getLinkPreview(link, {
|
||||
timeout: 6000,
|
||||
headers: {
|
||||
'user-agent': 'googlebot',
|
||||
},
|
||||
followRedirects: 'follow',
|
||||
}).catch(() => {
|
||||
return {
|
||||
title: '',
|
||||
siteName: '',
|
||||
description: '',
|
||||
images: [],
|
||||
videos: [],
|
||||
contentType: `text/html`,
|
||||
favicons: [],
|
||||
};
|
||||
})) as any;
|
||||
|
||||
return {
|
||||
title: previewData.title,
|
||||
description: previewData.description,
|
||||
icon: previewData.favicons[0],
|
||||
image: previewData.images[0],
|
||||
};
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { join, resolve } from 'node:path';
|
||||
import { parentPort } from 'node:worker_threads';
|
||||
|
||||
import type { ServerContext } from '@affine/sdk/server';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error('parentPort is null');
|
||||
}
|
||||
const commandProxy: Record<string, (...args: any[]) => Promise<any>> = {};
|
||||
|
||||
parentPort.start();
|
||||
|
||||
const mainThread = AsyncCall<{
|
||||
log: (...args: any[]) => Promise<void>;
|
||||
}>(commandProxy, {
|
||||
channel: new MessageEventChannel(parentPort),
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
globalThis.console.log = mainThread.log;
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
globalThis.console.error = mainThread.log;
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
globalThis.console.info = mainThread.log;
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
globalThis.console.debug = mainThread.log;
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
globalThis.console.warn = mainThread.log;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const bookmarkPluginModule = require(
|
||||
join(
|
||||
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
|
||||
'./bookmark/index.cjs'
|
||||
)
|
||||
);
|
||||
|
||||
const serverContext: ServerContext = {
|
||||
registerCommand: (command, fn) => {
|
||||
console.log('register command', command);
|
||||
commandProxy[command] = fn;
|
||||
},
|
||||
unregisterCommand: command => {
|
||||
console.log('unregister command', command);
|
||||
delete commandProxy[command];
|
||||
},
|
||||
};
|
||||
|
||||
bookmarkPluginModule.entry(serverContext);
|
||||
Reference in New Issue
Block a user