зеркало из https://github.com/mozilla/email-tabs.git
Initial work
This commit is contained in:
Родитель
920f85275c
Коммит
35d3d989df
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"plugins": ["transform-react-jsx"],
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/addon/build
|
|
@ -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"
|
||||
}
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
/Profile
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
/web-ext-artifacts
|
||||
/addon/build
|
||||
/addon.xpi
|
22
README.md
22
README.md
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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})
|
||||
};
|
||||
})()
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
|
@ -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'"
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче