This commit is contained in:
Ian Bicking 2018-04-10 18:00:18 -05:00
Родитель 920f85275c
Коммит 35d3d989df
12 изменённых файлов: 382 добавлений и 2 удалений

3
.babelrc Normal file
Просмотреть файл

@ -0,0 +1,3 @@
{
"plugins": ["transform-react-jsx"],
}

1
.eslintignore Normal file
Просмотреть файл

@ -0,0 +1 @@
/addon/build

26
.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,26 @@
module.exports = {
"env": {
"es6": true,
"webextensions": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:mozilla/recommended"
],
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module"
},
"plugins": [
"mozilla"
],
"root": true,
"rules": {
"eqeqeq": "error",
"no-console": "warn",
"space-before-function-paren": "off",
"no-console": ["error", {"allow": ["error", "info", "trace", "warn"]}],
"react/prop-types": "off"
}
};

6
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,6 @@
/Profile
/node_modules
/package-lock.json
/web-ext-artifacts
/addon/build
/addon.xpi

Просмотреть файл

@ -1,2 +1,20 @@
# email-tabs
An experimental add-on to email a list of all your tabs
# Copy Keeper
This is an experimental extension for Firefox that saves everything you copy to the clipboard.
The data is saved locally, and can be viewed in a sidebar.
In addition to the text you copy, the source page, and a screenshot is captured.
Old clips are searchable.
## Install
To install and test out:
```sh
$ git clone https://github.com/ianb/copy-keeper.git
$ cd copy-keeper
$ npm install
$ npm start
```

55
addon/background.js Normal file
Просмотреть файл

@ -0,0 +1,55 @@
browser.contextMenus.create({
id: "email-tabs",
title: "Email tabs...",
contexts: ["page", "tab"],
documentUrlPatterns: ["<all_urls>"]
});
browser.browserAction.onClicked.addListener(async () => {
browser.sidebarAction.open();
});
browser.contextMenus.onClicked.addListener(async (info, tab) => {
browser.sidebarAction.open();
});
browser.runtime.onMessage.addListener((message, source) => {
if (message.type === "sendEmail") {
sendEmail(message.tabIds);
} else {
console.error("Unexpected message type:", message.type);
}
});
async function sendEmail(tabIds) {
let allTabs = await browser.tabs.query({});
let tabInfo = {};
for (let tab of allTabs) {
if (tabIds.includes(tab.id)) {
tabInfo[tab.id] = {url: tab.url, title: tab.title, favIcon: tab.favIconUrl, id: tab.id};
}
}
for (let tabId of tabIds) {
let data = await browser.tabs.executeScript(tabId, {
file: "capture-data.js",
});
console.log("got data", data[0]);
Object.assign(tabInfo[tabId], data[0]);
}
let html = await browser.runtime.sendMessage({
type: "renderRequest",
tabs: tabIds.map(id => tabInfo[id])
});
console.log("going to create new tab");
let newTab = await browser.tabs.create({url: "https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&to="});
console.log("got it!", newTab.id);
await browser.tabs.executeScript(newTab.id, {
file: "set-html-email.js",
});
browser.tabs.sendMessage(newTab.id, {
type: "setHtml",
html
});
console.log("Result:", tabInfo);
console.log("sending email for tabs...", tabIds);
}

32
addon/capture-data.js Normal file
Просмотреть файл

@ -0,0 +1,32 @@
(function () {
let title = document.title;
for (let el of document.querySelectorAll("meta[name='twitter:title'], meta[name='og:title']")) {
title = el.getAttribute("content") || title;
}
let url = location.href;
for (let el of document.querySelectorAll("link[rel='canonical']")) {
url = el.getAttribute("href") || url;
}
function screenshotBox(box) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
box.width = box.width || box.right - box.left;
box.height = box.height || box.bottom - box.top;
canvas.width = box.width * window.devicePixelRatio;
canvas.height = box.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
ctx.drawWindow(window, box.left, box.top, box.width, box.height, "#fff");
return {
url: canvas.toDataURL(),
height: box.height,
width: box.width,
};
}
return {
title,
url,
screenshot: screenshotBox({left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight})
};
})()

35
addon/manifest.json Normal file
Просмотреть файл

@ -0,0 +1,35 @@
{
"manifest_version": 2,
"name": "Email Tabs",
"version": "0.1",
"description": "Email a list of your current tabs",
"author": "Ian Bicking (http://www.ianbicking.org)",
"homepage_url": "https://github.com/ianb/email-tabs/",
"applications": {
"gecko": {
"id": "email-tabs@mozilla.org",
"strict_min_version": "57.0a1"
}
},
"background": {
"scripts": [
"background.js"
]
},
"browser_action": {
"default_icon": "side-view.svg",
"default_title": "Open the copy drawer"
},
"sidebar_action": {
"default_title": "Email Tabs",
"default_panel": "sidebar.html"
},
"web_accessible_resources": [
],
"permissions": [
"activeTab",
"tabs",
"<all_urls>",
"contextMenus"
]
}

13
addon/set-html-email.js Normal file
Просмотреть файл

@ -0,0 +1,13 @@
browser.runtime.onMessage.addListener((message) => {
console.log("got message", message.html.length);
setHtml(message.html);
});
function setHtml(html) {
let editableEl = document.querySelector("div.editable[contenteditable]");
if (!editableEl) {
setTimeout(setHtml.bind(this, html), 100);
return;
}
editableEl.innerHTML = html + "\n<br />" + editableEl.innerHTML;
}

16
addon/sidebar.html Normal file
Просмотреть файл

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Email Tabs</title>
<link rel="stylesheet" href="sidebar.css">
</head>
<body>
<div id="container">
</div>
<script src="build/react.production.min.js"></script>
<script src="build/react-dom.production.min.js"></script>
<script src="build/react-dom-server.browser.production.min.js"></script>
<script src="build/sidebar.js"></script>
</body>
</html>

125
addon/sidebar.jsx Normal file
Просмотреть файл

@ -0,0 +1,125 @@
/* globals React, ReactDOM */
let searchTerm;
let selected = new Map();
class Tab extends React.Component {
render() {
let tab = this.props.tab;
let checkId = `checkbox-${this.props.tab.id}`;
let isOkay = tab.url.startsWith("http");
let checked = this.props.selected.get(tab.id);
return <li>
<label htmlFor={checkId}>
{ isOkay ? <input type="checkbox" value={tab.id} checked={checked}
onChange={this.onChange.bind(this)} id={checkId} ref={checkbox => this.checkbox = checkbox} /> : null }
<img height="16" width="16" src={tab.favIconUrl} />
<span>{tab.title}</span>
</label>
</li>;
}
onChange() {
selected.set(this.props.tab.id, this.checkbox.checked);
render();
}
}
class TabList extends React.Component {
render() {
let tabElements = this.props.tabs.map(
tab => <Tab tab={tab} key={tab.id} selected={this.props.selected} />
);
return <ul>{tabElements}</ul>;
}
}
class Page extends React.Component {
render() {
return <div>
<div>
<input type="text" placeholder="Search" value={this.props.searchTerm} onChange={this.onChangeSearch.bind(this)} ref={search => this.search = search} />
<button onClick={this.onClickCheckAll.bind(this)}>Check all/none</button>
<button onClick={this.sendEmail.bind(this)}>Send email</button>
</div>
<TabList tabs={this.props.tabs} selected={this.props.selected} />
</div>;
}
onChangeSearch() {
searchTerm = this.search.value;
render();
}
onClickCheckAll() {
let allChecked = true;
for (let tab of this.props.tabs) {
allChecked = allChecked && this.props.selected.get(tab.id);
}
for (let tab of this.props.tabs) {
selected.set(tab.id, !allChecked);
}
render();
}
sendEmail() {
let sendTabs = this.props.tabs.filter(tab => this.props.selected.get(tab.id));
sendTabs = sendTabs.map(tab => tab.id);
browser.runtime.sendMessage({
type: "sendEmail",
tabIds: sendTabs,
});
}
}
class Email extends React.Component {
render() {
let tabList = this.props.tabs.map(
tab => <EmailTab key={tab.id} tab={tab} />
);
return <div>{tabList}</div>;
}
}
class EmailTab extends React.Component {
render() {
let tab = this.props.tab;
return <div>
<a href={tab.url}>{tab.title}</a> <br />
<img height={tab.screenshot.height} width={tab.screenshot.width} src={tab.screenshot.url} />
</div>;
}
}
function searchTermMatches(tab, searchTerm) {
let caseInsensitive = searchTerm.toLowerCase() === searchTerm;
let match;
if (caseInsensitive) {
match = (a) => a.toLowerCase().includes(searchTerm);
} else {
match = (a) => a.includes(searchTerm);
}
return match(tab.title) || match(tab.url);
}
async function render() {
let tabs = await browser.tabs.query({});
if (searchTerm) {
tabs = tabs.filter(tab => searchTermMatches(tab, searchTerm));
}
let page = <Page selected={selected} searchTerm={searchTerm} tabs={tabs} />;
ReactDOM.render(page, document.getElementById("container"));
}
for (let eventName of ["onAttached", "onCreated", "onDetached", "onMoved", "onUpdated"]) {
browser.tabs[eventName].addListener(render);
}
browser.runtime.onMessage.addListener((message) => {
if (message.type == "renderRequest") {
let emailHtml = ReactDOMServer.renderToStaticMarkup(<Email tabs={message.tabs} />);
return Promise.resolve(emailHtml);
}
});
render();

50
package.json Normal file
Просмотреть файл

@ -0,0 +1,50 @@
{
"name": "email-tabs",
"description": "Experimental extension to email a list of your tabs",
"version": "0.1.0",
"author": "Ian Bicking <ian@ianbicking.org>",
"bugs": {
"url": "https://github.com/ianb/email-tabs"
},
"dependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"nodemon": "^1.17.3",
"react": "^16.3.1",
"react-dom": "^16.3.1"
},
"devDependencies": {
"addons-linter": "^0.37.0",
"eslint": "^4.16.0",
"eslint-plugin-mozilla": "^0.6.0",
"eslint-plugin-no-unsanitized": "^2.0.2",
"eslint-plugin-react": "^7.7.0",
"npm-run-all": "^4.1.2",
"stylelint": "^9.1.1",
"stylelint-config-standard": "^18.2.0",
"web-ext": "^2.4.0"
},
"homepage": "https://github.com/ianb/email-tabs/",
"license": "MPL-2.0",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/ianb/email-tabs.git"
},
"scripts": {
"start": "npm-run-all build run",
"lint": "npm-run-all lint:*",
"lint:addon": "npm run package && addons-linter ./addon.xpi -o text",
"lint:js": "eslint addon",
"lint:styles": "stylelint ./addon/*.css",
"build": "npm-run-all build:*",
"build:deps": "mkdir -p addon/build/ && cp node_modules/react/umd/react.production.min.js node_modules/react-dom/umd/react-dom.production.min.js node_modules/react-dom/umd/react-dom-server.browser.production.min.js addon/build/ && babel --retain-lines addon/sidebar.jsx > addon/build/sidebar.js",
"build:web-ext": "web-ext build --source-dir=addon --overwrite-dest --ignore-files '*.tmpl'",
"package": "npm run build && cp web-ext-artifacts/`ls -t1 web-ext-artifacts | head -n 1` addon.xpi",
"run": "mkdir -p ./Profile && npm run build:deps && web-ext run --source-dir=addon -p ./Profile --browser-console --keep-profile-changes -f ${FIREFOX:-nightly}",
"test": "npm run lint",
"postinstall": "npm run build",
"watch": "nodemon -e jsx -w addon/ --exec 'npm run build:deps'"
}
}