chore: add clipper extension package (#2281)

This commit is contained in:
Aman Harwara
2023-04-11 22:14:02 +05:30
committed by GitHub
parent 0b0466c9fa
commit 4f5e634685
214 changed files with 3163 additions and 355 deletions

View File

@@ -0,0 +1,32 @@
import { runtime, action, browserAction, windows, storage } from 'webextension-polyfill'
import { RuntimeMessage, RuntimeMessageTypes } from '../types/message'
const isFirefox = navigator.userAgent.indexOf('Firefox/') !== -1
const openPopupAndClipSelection = async (payload: { title: string; content: string }) => {
await storage.local.set({ clip: payload })
if (isFirefox) {
const popupURL = await browserAction.getPopup({})
await windows.create({
type: 'detached_panel',
url: popupURL,
width: 350,
height: 450,
})
return
}
const openPopup = runtime.getManifest().manifest_version === 3 ? action.openPopup : browserAction.openPopup
void openPopup()
}
runtime.onMessage.addListener((message: RuntimeMessage) => {
if (message.type === RuntimeMessageTypes.OpenPopupWithSelection) {
if (!message.payload) {
return
}
void openPopupAndClipSelection(message.payload)
}
})

View File

@@ -0,0 +1,119 @@
import { runtime } from 'webextension-polyfill'
import { Readability } from '@mozilla/readability'
import { RuntimeMessage, RuntimeMessageTypes } from '../types/message'
let isSelectingNodeForClipping = false
runtime.onMessage.addListener(async (message: RuntimeMessage) => {
switch (message.type) {
case RuntimeMessageTypes.StartNodeSelection: {
isSelectingNodeForClipping = true
return
}
case RuntimeMessageTypes.HasSelection: {
const selection = window.getSelection()
if (!selection) {
return false
}
if (selection.rangeCount < 1) {
return false
}
const range = selection.getRangeAt(0)
return !range.collapsed
}
case RuntimeMessageTypes.GetSelection: {
const selection = window.getSelection()
if (!selection || selection.rangeCount < 1) {
return
}
const range = selection.getRangeAt(0)
const result = document.createElement('div')
result.appendChild(range.cloneContents())
return { title: document.title, content: result.innerHTML, url: window.location.href }
}
case RuntimeMessageTypes.GetFullPage: {
return { title: document.title, content: document.body.innerHTML, url: window.location.href }
}
case RuntimeMessageTypes.GetArticle: {
const documentClone = document.cloneNode(true) as Document
const article = new Readability(documentClone).parse()
if (!article) {
return
}
return { title: article.title, content: article.content, url: window.location.href }
}
default:
return
}
})
const nodeOverlayElement = document.createElement('div')
nodeOverlayElement.style.border = '2px solid #086dd6'
nodeOverlayElement.style.position = 'fixed'
nodeOverlayElement.style.top = '0'
nodeOverlayElement.style.left = '0'
nodeOverlayElement.style.zIndex = '69420'
nodeOverlayElement.style.width = window.innerWidth + 'px'
nodeOverlayElement.style.height = window.innerHeight - 4 + 'px'
nodeOverlayElement.style.pointerEvents = 'none'
nodeOverlayElement.style.visibility = 'hidden'
nodeOverlayElement.id = 'sn-clipper-node-overlay'
document.body.appendChild(nodeOverlayElement)
window.addEventListener('mousemove', (event) => {
if (!isSelectingNodeForClipping) {
nodeOverlayElement.style.visibility = 'hidden'
return
}
nodeOverlayElement.style.visibility = ''
const { target } = event
if (!target || !(target instanceof HTMLElement)) {
return
}
const targetRect = target.getBoundingClientRect()
nodeOverlayElement.style.width = targetRect.width + 'px'
nodeOverlayElement.style.height = targetRect.height + 'px'
nodeOverlayElement.style.transform = `translate3d(${targetRect.x}px, ${targetRect.y}px, 0)`
})
const disableNodeSelection = () => {
isSelectingNodeForClipping = false
nodeOverlayElement.style.visibility = 'hidden'
}
window.addEventListener('click', (event) => {
if (!isSelectingNodeForClipping) {
return
}
disableNodeSelection()
event.preventDefault()
event.stopPropagation()
const { target } = event
if (!target || !(target instanceof HTMLElement)) {
return
}
const title = document.title
const content = target.outerHTML
void runtime.sendMessage({
type: RuntimeMessageTypes.OpenPopupWithSelection,
payload: { title, content, url: window.location.href },
} as RuntimeMessage)
})
window.addEventListener('keydown', (event) => {
if (!isSelectingNodeForClipping) {
return
}
if (event.key === 'Escape') {
disableNodeSelection()
}
})

View File

@@ -0,0 +1,33 @@
{
"manifest_version": 2,
"name": "Standard Notes Clipper",
"description": "Web clipper for Standard Notes",
"permissions": ["activeTab", "storage"],
"browser_action": {
"default_popup": "popup/index.html?route=extension"
},
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}
],
"content_security_policy": "default-src 'self'; script-src 'self' 'wasm-eval' 'wasm-unsafe-eval'; worker-src blob:; connect-src * data: blob:; style-src 'unsafe-inline' 'self'; frame-src * blob:; img-src * data: blob:;",
"icons": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"96": "images/icon96.png",
"128": "images/icon128.png"
},
"browser_specific_settings": {
"gecko": {
"id": "{9f917dfe-accd-4d3a-9685-33c3ac0ca643}",
"strict_min_version": "48.0"
}
}
}

View File

@@ -0,0 +1,34 @@
{
"manifest_version": 3,
"name": "Standard Notes Clipper",
"description": "Web clipper for Standard Notes",
"permissions": ["activeTab", "storage"],
"action": {
"default_popup": "popup/index.html?route=extension"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"]
}
],
"icons": {
"16": "images/icon16.png",
"32": "images/icon32.png",
"48": "images/icon48.png",
"96": "images/icon96.png",
"128": "images/icon128.png"
},
"content_security_policy": {
"extension_pages": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src * data: blob:; style-src 'unsafe-inline' 'self'; frame-src * blob:; img-src * data: blob:;"
},
"browser_specific_settings": {
"gecko": {
"id": "{9f917dfe-accd-4d3a-9685-33c3ac0ca643}",
"strict_min_version": "48.0"
}
}
}

View File

@@ -0,0 +1,7 @@
window.defaultSyncServer = 'https://api.standardnotes.com'
window.defaultFilesHost = 'https://files.standardnotes.com'
window.enabledUnfinishedFeatures = false
window.websocketUrl = 'wss://sockets.standardnotes.com'
window.purchaseUrl = 'https://standardnotes.com/purchase'
window.plansUrl = 'https://standardnotes.com/plans'
window.dashboardUrl = 'https://standardnotes.com/dashboard'

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
<meta content="viewport-fit=cover, width=device-width, initial-scale=1" name="viewport" />
<meta name="theme-color" content="#ffffff" />
<title>Standard Notes</title>
<script src="./globals.js"></script>
<script src="../web/app.js" debug="false"></script>
<link rel="stylesheet" media="all" href="../web/app.css" debug="false" />
<style>
html,
body {
min-width: 350px;
max-width: 350px;
}
</style>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,36 @@
export const RuntimeMessageTypes = {
GetArticle: 'get-article',
GetSelection: 'get-selection',
HasSelection: 'has-selection',
GetFullPage: 'get-full-page',
OpenPopupWithSelection: 'open-popup-with-selection',
StartNodeSelection: 'start-node-selection',
} as const
export type RuntimeMessageType = typeof RuntimeMessageTypes[keyof typeof RuntimeMessageTypes]
type MessagesWithClipPayload = typeof RuntimeMessageTypes.OpenPopupWithSelection
export type ClipPayload = {
title: string
content: string
url: string
}
export type RuntimeMessageReturnTypes = {
[RuntimeMessageTypes.GetArticle]: ClipPayload
[RuntimeMessageTypes.GetSelection]: ClipPayload
[RuntimeMessageTypes.HasSelection]: boolean
[RuntimeMessageTypes.GetFullPage]: ClipPayload
[RuntimeMessageTypes.OpenPopupWithSelection]: void
[RuntimeMessageTypes.StartNodeSelection]: void
}
export type RuntimeMessage =
| {
type: MessagesWithClipPayload
payload: ClipPayload
}
| {
type: Exclude<RuntimeMessageType, MessagesWithClipPayload>
}

View File

@@ -0,0 +1,14 @@
import { tabs } from 'webextension-polyfill'
import { RuntimeMessageReturnTypes, RuntimeMessageType } from '../types/message'
export default async function sendMessageToActiveTab<T extends RuntimeMessageType>(
type: T,
): Promise<RuntimeMessageReturnTypes[T] | undefined> {
const [activeTab] = await tabs.query({ active: true, currentWindow: true, windowType: 'normal' })
if (!activeTab || !activeTab.id) {
return
}
return await tabs.sendMessage(activeTab.id, { type })
}