diff --git a/.yarn/cache/html-to-image-npm-1.11.11-faab8eba97-b453beca72.zip b/.yarn/cache/html-to-image-npm-1.11.11-faab8eba97-b453beca72.zip new file mode 100644 index 000000000..133b21fcb Binary files /dev/null and b/.yarn/cache/html-to-image-npm-1.11.11-faab8eba97-b453beca72.zip differ diff --git a/packages/clipper/package.json b/packages/clipper/package.json index ec876ac6d..55f64956e 100644 --- a/packages/clipper/package.json +++ b/packages/clipper/package.json @@ -32,6 +32,7 @@ "webpack": "*" }, "dependencies": { - "@mozilla/readability": "^0.4.2" + "@mozilla/readability": "^0.4.2", + "html-to-image": "^1.11.11" } } diff --git a/packages/clipper/src/background/background.ts b/packages/clipper/src/background/background.ts index 0ada6e1c7..c4a969e1c 100644 --- a/packages/clipper/src/background/background.ts +++ b/packages/clipper/src/background/background.ts @@ -1,9 +1,9 @@ import { runtime, action, browserAction, windows, storage } from 'webextension-polyfill' -import { RuntimeMessage, RuntimeMessageTypes } from '../types/message' +import { ClipPayload, RuntimeMessage, RuntimeMessageTypes } from '../types/message' const isFirefox = navigator.userAgent.indexOf('Firefox/') !== -1 -const openPopupAndClipSelection = async (payload: { title: string; content: string }) => { +const openPopupAndClipSelection = async (payload: ClipPayload) => { await storage.local.set({ clip: payload }) if (isFirefox) { diff --git a/packages/clipper/src/content/content.ts b/packages/clipper/src/content/content.ts index 3b4fe1e09..376faf33c 100644 --- a/packages/clipper/src/content/content.ts +++ b/packages/clipper/src/content/content.ts @@ -1,8 +1,10 @@ import { runtime } from 'webextension-polyfill' import { Readability } from '@mozilla/readability' import { RuntimeMessage, RuntimeMessageTypes } from '../types/message' +import { toPng } from 'html-to-image' let isSelectingNodeForClipping = false +let isScreenshotMode = false runtime.onMessage.addListener(async (message: RuntimeMessage) => { switch (message.type) { @@ -10,6 +12,10 @@ runtime.onMessage.addListener(async (message: RuntimeMessage) => { isSelectingNodeForClipping = true return } + case RuntimeMessageTypes.ToggleScreenshotMode: { + isScreenshotMode = message.enabled + return + } case RuntimeMessageTypes.HasSelection: { const selection = window.getSelection() @@ -40,6 +46,11 @@ runtime.onMessage.addListener(async (message: RuntimeMessage) => { return { title: document.title, content: result.innerHTML, url: window.location.href } } case RuntimeMessageTypes.GetFullPage: { + if (isScreenshotMode) { + const content = await toPng(document.body) + return { title: document.title, content: content, url: window.location.href, isScreenshot: true } + } + return { title: document.title, content: document.body.innerHTML, url: window.location.href } } case RuntimeMessageTypes.GetArticle: { @@ -90,7 +101,7 @@ const disableNodeSelection = () => { nodeOverlayElement.style.visibility = 'hidden' } -window.addEventListener('click', (event) => { +window.addEventListener('click', async (event) => { if (!isSelectingNodeForClipping) { return } @@ -102,10 +113,10 @@ window.addEventListener('click', (event) => { return } const title = document.title - const content = target.outerHTML + const content = isScreenshotMode ? await toPng(target) : target.outerHTML void runtime.sendMessage({ type: RuntimeMessageTypes.OpenPopupWithSelection, - payload: { title, content, url: window.location.href }, + payload: { title, content, url: window.location.href, isScreenshot: isScreenshotMode }, } as RuntimeMessage) }) diff --git a/packages/clipper/src/types/message.ts b/packages/clipper/src/types/message.ts index 881f27870..c6303d5c5 100644 --- a/packages/clipper/src/types/message.ts +++ b/packages/clipper/src/types/message.ts @@ -5,6 +5,7 @@ export const RuntimeMessageTypes = { GetFullPage: 'get-full-page', OpenPopupWithSelection: 'open-popup-with-selection', StartNodeSelection: 'start-node-selection', + ToggleScreenshotMode: 'toggle-screenshot-mode', } as const export type RuntimeMessageType = typeof RuntimeMessageTypes[keyof typeof RuntimeMessageTypes] @@ -15,6 +16,7 @@ export type ClipPayload = { title: string content: string url: string + isScreenshot?: boolean } export type RuntimeMessageReturnTypes = { @@ -24,6 +26,7 @@ export type RuntimeMessageReturnTypes = { [RuntimeMessageTypes.GetFullPage]: ClipPayload [RuntimeMessageTypes.OpenPopupWithSelection]: void [RuntimeMessageTypes.StartNodeSelection]: void + [RuntimeMessageTypes.ToggleScreenshotMode]: void } export type RuntimeMessage = @@ -32,5 +35,9 @@ export type RuntimeMessage = payload: ClipPayload } | { - type: Exclude + type: typeof RuntimeMessageTypes.ToggleScreenshotMode + enabled: boolean + } + | { + type: Exclude } diff --git a/packages/clipper/src/utils/sendMessageToActiveTab.ts b/packages/clipper/src/utils/sendMessageToActiveTab.ts index b4c94d11b..a7e5130de 100644 --- a/packages/clipper/src/utils/sendMessageToActiveTab.ts +++ b/packages/clipper/src/utils/sendMessageToActiveTab.ts @@ -1,14 +1,14 @@ import { tabs } from 'webextension-polyfill' -import { RuntimeMessageReturnTypes, RuntimeMessageType } from '../types/message' +import { RuntimeMessage, RuntimeMessageReturnTypes } from '../types/message' -export default async function sendMessageToActiveTab( - type: T, -): Promise { +export default async function sendMessageToActiveTab( + message: T, +): Promise { const [activeTab] = await tabs.query({ active: true, currentWindow: true, windowType: 'normal' }) if (!activeTab || !activeTab.id) { return } - return await tabs.sendMessage(activeTab.id, { type }) + return await tabs.sendMessage(activeTab.id, message) } diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index 637febc7d..a439bb644 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -200,6 +200,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio applicationGroup={mainApplicationGroup} /> + {renderChallenges()} diff --git a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx index 701dfbb7b..512e06f14 100644 --- a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx @@ -36,6 +36,7 @@ import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown' import LinkedItemBubble from '../LinkedItems/LinkedItemBubble' import StyledTooltip from '../StyledTooltip/StyledTooltip' +import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem' const Header = () => (
@@ -135,6 +136,14 @@ const ClipperView = ({ } }, [application.user]) + const [isScreenshotMode, setIsScreenshotMode] = useState(false) + useEffect(() => { + void sendMessageToActiveTab({ + type: RuntimeMessageTypes.ToggleScreenshotMode, + enabled: isScreenshotMode, + }) + }, [isScreenshotMode]) + const [hasSelection, setHasSelection] = useState(false) useEffect(() => { if (!user) { @@ -143,7 +152,7 @@ const ClipperView = ({ try { const checkIfPageHasSelection = async () => { - setHasSelection(Boolean(await sendMessageToActiveTab(RuntimeMessageTypes.HasSelection))) + setHasSelection(Boolean(await sendMessageToActiveTab({ type: RuntimeMessageTypes.HasSelection }))) } void checkIfPageHasSelection() @@ -188,6 +197,17 @@ const ClipperView = ({ }) return } + if (clipPayload.isScreenshot) { + const blob = await fetch(clipPayload.content).then((response) => response.blob()) + + const file = new File([blob], `${clipPayload.title} - ${clipPayload.url}.png`, { + type: 'image/png', + }) + + viewControllerManager.filesController.uploadNewFile(file).catch(console.error) + + return + } const editorStateJSON = await getSuperJSONFromClipPayload(clipPayload) @@ -216,7 +236,15 @@ const ClipperView = ({ } void createNoteFromClip() - }, [application.items, application.linkingController, application.sync, clipPayload, defaultTag, isEntitledRef]) + }, [ + application.items, + application.linkingController, + application.sync, + clipPayload, + defaultTag, + isEntitledRef, + viewControllerManager.filesController, + ]) const upgradePlan = useCallback(async () => { if (hasSubscription) { @@ -307,18 +335,25 @@ const ClipperView = ({ { - const payload = await sendMessageToActiveTab(RuntimeMessageTypes.GetFullPage) + const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetFullPage }) if (!payload) { return } setClipPayload(payload) + if (isScreenshotMode) { + addToast({ + type: ToastType.Regular, + message: 'Capturing full page...', + }) + } }} > Clip full page { - const payload = await sendMessageToActiveTab(RuntimeMessageTypes.GetArticle) + const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetArticle }) if (!payload) { return } @@ -328,25 +363,34 @@ const ClipperView = ({ Clip article { - const payload = await sendMessageToActiveTab(RuntimeMessageTypes.GetSelection) + const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetSelection }) if (!payload) { return } setClipPayload(payload) }} > - Clip current selection + Clip text selection { - void sendMessageToActiveTab(RuntimeMessageTypes.StartNodeSelection) + void sendMessageToActiveTab({ type: RuntimeMessageTypes.StartNodeSelection }) window.close() }} > Select elements to clip + + Clip as screenshot +
Default tag:
diff --git a/packages/web/src/javascripts/Components/ClipperView/getSuperJSONFromClipHTML.tsx b/packages/web/src/javascripts/Components/ClipperView/getSuperJSONFromClipHTML.tsx index 560950446..694ee3680 100644 --- a/packages/web/src/javascripts/Components/ClipperView/getSuperJSONFromClipHTML.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/getSuperJSONFromClipHTML.tsx @@ -29,6 +29,10 @@ export const getSuperJSONFromClipPayload = async (clipPayload: ClipPayload) => { $getRoot().select() $insertNodes(clipSourceParagraphNode) + if (typeof clipPayload.content !== 'string') { + throw new Error('Clip payload content is not a string') + } + const dom = parser.parseFromString(clipPayload.content, 'text/html') const generatedNodes = $generateNodesFromDOM(editor, dom) const nodesToInsert: LexicalNode[] = [] diff --git a/yarn.lock b/yarn.lock index d2677e25f..180427327 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4709,6 +4709,7 @@ __metadata: copy-webpack-plugin: 11.0.0 eslint: ^8.29.0 eslint-config-prettier: ^8.5.0 + html-to-image: ^1.11.11 ts-loader: ^9.4.2 typescript: "*" web-ext: ^7.5.0 @@ -13712,6 +13713,13 @@ __metadata: languageName: node linkType: hard +"html-to-image@npm:^1.11.11": + version: 1.11.11 + resolution: "html-to-image@npm:1.11.11" + checksum: b453beca72a697bf06fae4945e5460d1d9b1751e8569a0d721dda9485df1dde093938cc9bd9172b8df5fc23133a53a4d619777b3d22f7211cd8a67e3197ab4e8 + languageName: node + linkType: hard + "htmlparser2@npm:^8.0.1": version: 8.0.1 resolution: "htmlparser2@npm:8.0.1"