feat(clipper): Added "Clip as screenshot" switch to allow clipping content as a screenshot and save it to Files
This commit is contained in:
BIN
.yarn/cache/html-to-image-npm-1.11.11-faab8eba97-b453beca72.zip
vendored
Normal file
BIN
.yarn/cache/html-to-image-npm-1.11.11-faab8eba97-b453beca72.zip
vendored
Normal file
Binary file not shown.
@@ -32,6 +32,7 @@
|
|||||||
"webpack": "*"
|
"webpack": "*"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mozilla/readability": "^0.4.2"
|
"@mozilla/readability": "^0.4.2",
|
||||||
|
"html-to-image": "^1.11.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { runtime, action, browserAction, windows, storage } from 'webextension-polyfill'
|
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 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 })
|
await storage.local.set({ clip: payload })
|
||||||
|
|
||||||
if (isFirefox) {
|
if (isFirefox) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { runtime } from 'webextension-polyfill'
|
import { runtime } from 'webextension-polyfill'
|
||||||
import { Readability } from '@mozilla/readability'
|
import { Readability } from '@mozilla/readability'
|
||||||
import { RuntimeMessage, RuntimeMessageTypes } from '../types/message'
|
import { RuntimeMessage, RuntimeMessageTypes } from '../types/message'
|
||||||
|
import { toPng } from 'html-to-image'
|
||||||
|
|
||||||
let isSelectingNodeForClipping = false
|
let isSelectingNodeForClipping = false
|
||||||
|
let isScreenshotMode = false
|
||||||
|
|
||||||
runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
@@ -10,6 +12,10 @@ runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
|||||||
isSelectingNodeForClipping = true
|
isSelectingNodeForClipping = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case RuntimeMessageTypes.ToggleScreenshotMode: {
|
||||||
|
isScreenshotMode = message.enabled
|
||||||
|
return
|
||||||
|
}
|
||||||
case RuntimeMessageTypes.HasSelection: {
|
case RuntimeMessageTypes.HasSelection: {
|
||||||
const selection = window.getSelection()
|
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 }
|
return { title: document.title, content: result.innerHTML, url: window.location.href }
|
||||||
}
|
}
|
||||||
case RuntimeMessageTypes.GetFullPage: {
|
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 }
|
return { title: document.title, content: document.body.innerHTML, url: window.location.href }
|
||||||
}
|
}
|
||||||
case RuntimeMessageTypes.GetArticle: {
|
case RuntimeMessageTypes.GetArticle: {
|
||||||
@@ -90,7 +101,7 @@ const disableNodeSelection = () => {
|
|||||||
nodeOverlayElement.style.visibility = 'hidden'
|
nodeOverlayElement.style.visibility = 'hidden'
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('click', (event) => {
|
window.addEventListener('click', async (event) => {
|
||||||
if (!isSelectingNodeForClipping) {
|
if (!isSelectingNodeForClipping) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -102,10 +113,10 @@ window.addEventListener('click', (event) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const title = document.title
|
const title = document.title
|
||||||
const content = target.outerHTML
|
const content = isScreenshotMode ? await toPng(target) : target.outerHTML
|
||||||
void runtime.sendMessage({
|
void runtime.sendMessage({
|
||||||
type: RuntimeMessageTypes.OpenPopupWithSelection,
|
type: RuntimeMessageTypes.OpenPopupWithSelection,
|
||||||
payload: { title, content, url: window.location.href },
|
payload: { title, content, url: window.location.href, isScreenshot: isScreenshotMode },
|
||||||
} as RuntimeMessage)
|
} as RuntimeMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const RuntimeMessageTypes = {
|
|||||||
GetFullPage: 'get-full-page',
|
GetFullPage: 'get-full-page',
|
||||||
OpenPopupWithSelection: 'open-popup-with-selection',
|
OpenPopupWithSelection: 'open-popup-with-selection',
|
||||||
StartNodeSelection: 'start-node-selection',
|
StartNodeSelection: 'start-node-selection',
|
||||||
|
ToggleScreenshotMode: 'toggle-screenshot-mode',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type RuntimeMessageType = typeof RuntimeMessageTypes[keyof typeof RuntimeMessageTypes]
|
export type RuntimeMessageType = typeof RuntimeMessageTypes[keyof typeof RuntimeMessageTypes]
|
||||||
@@ -15,6 +16,7 @@ export type ClipPayload = {
|
|||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
url: string
|
url: string
|
||||||
|
isScreenshot?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeMessageReturnTypes = {
|
export type RuntimeMessageReturnTypes = {
|
||||||
@@ -24,6 +26,7 @@ export type RuntimeMessageReturnTypes = {
|
|||||||
[RuntimeMessageTypes.GetFullPage]: ClipPayload
|
[RuntimeMessageTypes.GetFullPage]: ClipPayload
|
||||||
[RuntimeMessageTypes.OpenPopupWithSelection]: void
|
[RuntimeMessageTypes.OpenPopupWithSelection]: void
|
||||||
[RuntimeMessageTypes.StartNodeSelection]: void
|
[RuntimeMessageTypes.StartNodeSelection]: void
|
||||||
|
[RuntimeMessageTypes.ToggleScreenshotMode]: void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeMessage =
|
export type RuntimeMessage =
|
||||||
@@ -32,5 +35,9 @@ export type RuntimeMessage =
|
|||||||
payload: ClipPayload
|
payload: ClipPayload
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: Exclude<RuntimeMessageType, MessagesWithClipPayload>
|
type: typeof RuntimeMessageTypes.ToggleScreenshotMode
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: Exclude<RuntimeMessageType, MessagesWithClipPayload | typeof RuntimeMessageTypes.ToggleScreenshotMode>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { tabs } from 'webextension-polyfill'
|
import { tabs } from 'webextension-polyfill'
|
||||||
import { RuntimeMessageReturnTypes, RuntimeMessageType } from '../types/message'
|
import { RuntimeMessage, RuntimeMessageReturnTypes } from '../types/message'
|
||||||
|
|
||||||
export default async function sendMessageToActiveTab<T extends RuntimeMessageType>(
|
export default async function sendMessageToActiveTab<T extends RuntimeMessage>(
|
||||||
type: T,
|
message: T,
|
||||||
): Promise<RuntimeMessageReturnTypes[T] | undefined> {
|
): Promise<RuntimeMessageReturnTypes[T['type']] | undefined> {
|
||||||
const [activeTab] = await tabs.query({ active: true, currentWindow: true, windowType: 'normal' })
|
const [activeTab] = await tabs.query({ active: true, currentWindow: true, windowType: 'normal' })
|
||||||
|
|
||||||
if (!activeTab || !activeTab.id) {
|
if (!activeTab || !activeTab.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await tabs.sendMessage(activeTab.id, { type })
|
return await tabs.sendMessage(activeTab.id, message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
applicationGroup={mainApplicationGroup}
|
applicationGroup={mainApplicationGroup}
|
||||||
/>
|
/>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||||
{renderChallenges()}
|
{renderChallenges()}
|
||||||
</FileDragNDropProvider>
|
</FileDragNDropProvider>
|
||||||
</LinkingControllerProvider>
|
</LinkingControllerProvider>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
|
|||||||
import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown'
|
import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown'
|
||||||
import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
||||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||||
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
|
|
||||||
const Header = () => (
|
const Header = () => (
|
||||||
<div className="flex items-center border-b border-border p-1 px-3 py-2 text-base font-semibold text-info-contrast">
|
<div className="flex items-center border-b border-border p-1 px-3 py-2 text-base font-semibold text-info-contrast">
|
||||||
@@ -135,6 +136,14 @@ const ClipperView = ({
|
|||||||
}
|
}
|
||||||
}, [application.user])
|
}, [application.user])
|
||||||
|
|
||||||
|
const [isScreenshotMode, setIsScreenshotMode] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
void sendMessageToActiveTab({
|
||||||
|
type: RuntimeMessageTypes.ToggleScreenshotMode,
|
||||||
|
enabled: isScreenshotMode,
|
||||||
|
})
|
||||||
|
}, [isScreenshotMode])
|
||||||
|
|
||||||
const [hasSelection, setHasSelection] = useState(false)
|
const [hasSelection, setHasSelection] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -143,7 +152,7 @@ const ClipperView = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const checkIfPageHasSelection = async () => {
|
const checkIfPageHasSelection = async () => {
|
||||||
setHasSelection(Boolean(await sendMessageToActiveTab(RuntimeMessageTypes.HasSelection)))
|
setHasSelection(Boolean(await sendMessageToActiveTab({ type: RuntimeMessageTypes.HasSelection })))
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkIfPageHasSelection()
|
void checkIfPageHasSelection()
|
||||||
@@ -188,6 +197,17 @@ const ClipperView = ({
|
|||||||
})
|
})
|
||||||
return
|
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)
|
const editorStateJSON = await getSuperJSONFromClipPayload(clipPayload)
|
||||||
|
|
||||||
@@ -216,7 +236,15 @@ const ClipperView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void createNoteFromClip()
|
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 () => {
|
const upgradePlan = useCallback(async () => {
|
||||||
if (hasSubscription) {
|
if (hasSubscription) {
|
||||||
@@ -307,18 +335,25 @@ const ClipperView = ({
|
|||||||
<Menu a11yLabel="Extension menu" isOpen={true} className="pb-1">
|
<Menu a11yLabel="Extension menu" isOpen={true} className="pb-1">
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const payload = await sendMessageToActiveTab(RuntimeMessageTypes.GetFullPage)
|
const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetFullPage })
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setClipPayload(payload)
|
setClipPayload(payload)
|
||||||
|
if (isScreenshotMode) {
|
||||||
|
addToast({
|
||||||
|
type: ToastType.Regular,
|
||||||
|
message: 'Capturing full page...',
|
||||||
|
})
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Clip full page
|
Clip full page
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
disabled={isScreenshotMode}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const payload = await sendMessageToActiveTab(RuntimeMessageTypes.GetArticle)
|
const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetArticle })
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -328,25 +363,34 @@ const ClipperView = ({
|
|||||||
Clip article
|
Clip article
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelection || isScreenshotMode}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const payload = await sendMessageToActiveTab(RuntimeMessageTypes.GetSelection)
|
const payload = await sendMessageToActiveTab({ type: RuntimeMessageTypes.GetSelection })
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setClipPayload(payload)
|
setClipPayload(payload)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Clip current selection
|
Clip text selection
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
void sendMessageToActiveTab(RuntimeMessageTypes.StartNodeSelection)
|
void sendMessageToActiveTab({ type: RuntimeMessageTypes.StartNodeSelection })
|
||||||
window.close()
|
window.close()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Select elements to clip
|
Select elements to clip
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuSwitchButtonItem
|
||||||
|
checked={isScreenshotMode}
|
||||||
|
onChange={function (checked: boolean): void {
|
||||||
|
setIsScreenshotMode(checked)
|
||||||
|
}}
|
||||||
|
className="flex-row-reverse gap-2"
|
||||||
|
>
|
||||||
|
Clip as screenshot
|
||||||
|
</MenuSwitchButtonItem>
|
||||||
<div className="border-t border-border px-3 py-3 text-base text-foreground">
|
<div className="border-t border-border px-3 py-3 text-base text-foreground">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="font-medium">Default tag:</div>
|
<div className="font-medium">Default tag:</div>
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export const getSuperJSONFromClipPayload = async (clipPayload: ClipPayload) => {
|
|||||||
$getRoot().select()
|
$getRoot().select()
|
||||||
$insertNodes(clipSourceParagraphNode)
|
$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 dom = parser.parseFromString(clipPayload.content, 'text/html')
|
||||||
const generatedNodes = $generateNodesFromDOM(editor, dom)
|
const generatedNodes = $generateNodesFromDOM(editor, dom)
|
||||||
const nodesToInsert: LexicalNode[] = []
|
const nodesToInsert: LexicalNode[] = []
|
||||||
|
|||||||
@@ -4709,6 +4709,7 @@ __metadata:
|
|||||||
copy-webpack-plugin: 11.0.0
|
copy-webpack-plugin: 11.0.0
|
||||||
eslint: ^8.29.0
|
eslint: ^8.29.0
|
||||||
eslint-config-prettier: ^8.5.0
|
eslint-config-prettier: ^8.5.0
|
||||||
|
html-to-image: ^1.11.11
|
||||||
ts-loader: ^9.4.2
|
ts-loader: ^9.4.2
|
||||||
typescript: "*"
|
typescript: "*"
|
||||||
web-ext: ^7.5.0
|
web-ext: ^7.5.0
|
||||||
@@ -13712,6 +13713,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"htmlparser2@npm:^8.0.1":
|
||||||
version: 8.0.1
|
version: 8.0.1
|
||||||
resolution: "htmlparser2@npm:8.0.1"
|
resolution: "htmlparser2@npm:8.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user