├── screenshots
├── gmailviewwatcher-inbox.jpg
├── gmailviewwatcher-compose.jpg
└── gmailviewwatcher-contacts.jpg
├── scripts
├── gmail-view-watcher.user.js
├── gmail-font-toggle.user.js
├── gmail-label-colors.user.js
├── gmail-reader.user.js
├── gmail-new-macros.user.js
├── gmail-macros.user.js
├── gmail-conversation-preview.user.js
└── gmail-saved-searches.user.js
├── README.md
└── LICENSE.txt
/screenshots/gmailviewwatcher-inbox.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mihaip/gmail-greasemonkey/HEAD/screenshots/gmailviewwatcher-inbox.jpg
--------------------------------------------------------------------------------
/screenshots/gmailviewwatcher-compose.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mihaip/gmail-greasemonkey/HEAD/screenshots/gmailviewwatcher-compose.jpg
--------------------------------------------------------------------------------
/screenshots/gmailviewwatcher-contacts.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mihaip/gmail-greasemonkey/HEAD/screenshots/gmailviewwatcher-contacts.jpg
--------------------------------------------------------------------------------
/scripts/gmail-view-watcher.user.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2007, Google Inc.
2 | // Released under the BSD license:
3 | // http://www.opensource.org/licenses/bsd-license.php
4 | //
5 | // ==UserScript==
6 | // @name Gmail View Watcher
7 | // @namespace http://mail.google.com/
8 | // @description Adds a nav box to Gmail which monitors the current view.
9 | // @include http://mail.google.com/*
10 | // @include https://mail.google.com/*
11 | // ==/UserScript==
12 |
13 | window.addEventListener('load', function() {
14 | if (unsafeWindow.gmonkey) {
15 | unsafeWindow.gmonkey.load('1.0', function(gmail) {
16 | function setViewType() {
17 | var str = '';
18 | switch (gmail.getActiveViewType()) {
19 | case 'tl': str = 'Threadlist'; break;
20 | case 'cv': str = 'Conversation'; break;
21 | case 'co': str = 'Compose'; break;
22 | case 'ct': str = 'Contacts'; break;
23 | case 's': str = 'Settings'; break;
24 | default: str = 'Unknown';
25 | }
26 | module.setContent(str);
27 | }
28 | var module = gmail.addNavModule('View Monitor');
29 | gmail.registerViewChangeCallback(setViewType);
30 | setViewType();
31 | });
32 | }
33 | }, true);
34 |
--------------------------------------------------------------------------------
/scripts/gmail-font-toggle.user.js:
--------------------------------------------------------------------------------
1 | // Copyright 2006 Mihai Parparita. All Rights Reserved.
2 |
3 | // ==UserScript==
4 | // @name Gmail Fixed Font Toggle
5 | // @namespace http://persistent.info/greasemonkey
6 | // @description Adds a fixed font size toggle button.
7 | // @include http://mail.google.com/*
8 | // @include https://mail.google.com/*
9 |
10 | // ==/UserScript==
11 |
12 | // Utility functions
13 | function getObjectMethodClosure(object, method) {
14 | return function() {
15 | return object[method].apply(object, arguments);
16 | }
17 | }
18 |
19 | // Shorthand
20 | var newNode = getObjectMethodClosure(document, "createElement");
21 | var getNode = getObjectMethodClosure(document, "getElementById");
22 |
23 | // Contants
24 | const MONOSPACE_RULE = ".mb, textarea.tb {font-family: monospace !important;}";
25 | const NORMAL_RULE = ".mb, textarea.tb {}";
26 |
27 | const TOGGLE_FONT_IMAGE = "data:image/gif;base64,R0lGODlhEAAQAIABAAAAzP%2F%2" +
28 | "F%2FyH5BAEAAAEALAAAAAAQABAAAAImjI%2BJwO28wIGG1rjUlFrZvoHJVz0SGXBqymXphU5" +
29 | "Y17Kg%2BnixKBYAOw%3D%3D";
30 |
31 | // Globals
32 | var styleSheet = null;
33 | var currentRule = NORMAL_RULE;
34 |
35 | var toggleFontLink = null;
36 |
37 | function initializeToggleFont() {
38 | var linksContainer = getNode("ap");
39 |
40 | if (!linksContainer) {
41 | return;
42 | }
43 |
44 | toggleFontLink = newNode("div");
45 | toggleFontLink.className = "ar";
46 | toggleFontLink.addEventListener("click", toggleMessageBodyFont, false);
47 | toggleFontLink.innerHTML =
48 | '' +
49 | '' +
50 | 'Toggle font' +
51 | '';
52 |
53 | linksContainer.appendChild(toggleFontLink);
54 |
55 | checkToggleFontParent();
56 | }
57 |
58 | function checkToggleFontParent() {
59 | if (toggleFontLink.parentNode != getNode("ap")) {
60 | getNode("ap").appendChild(toggleFontLink);
61 | }
62 |
63 | window.setTimeout(checkToggleFontParent, 200);
64 | }
65 |
66 | function toggleMessageBodyFont() {
67 | styleSheet.deleteRule(0);
68 | if (currentRule == NORMAL_RULE) {
69 | currentRule = MONOSPACE_RULE;
70 | } else {
71 | currentRule = NORMAL_RULE;
72 | }
73 | styleSheet.insertRule(currentRule, 0);
74 | }
75 |
76 | function initializeStyles() {
77 | var styleNode = newNode("style");
78 |
79 | document.body.appendChild(styleNode);
80 |
81 | styleSheet = document.styleSheets[document.styleSheets.length - 1];
82 |
83 | styleSheet.insertRule(NORMAL_RULE, 0);
84 | }
85 |
86 | initializeStyles();
87 | initializeToggleFont();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Various Greasemonkey scripts that enhance Gmail.
2 |
3 | N.B. It is highly unlikely that any of these scripts still work; "new version of Gmail" refers to the [late 2007 new version](http://gmailblog.blogspot.com/2007/10/code-changes-to-prepare-gmail-for.html). There was a [late 2011 revamp](http://gmailblog.blogspot.com/2011/11/gmails-new-look.html) that changed Gmail's UI significantly. On the plus side, nearly all of these features are now available as either built-in features or [Gmail Labs](http://support.google.com/mail/bin/answer.py?hl=en&answer=29418).
4 |
5 | # For the new version of Gmail
6 |
7 | * [Macros](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-new-macros.user.js): adds more keyboard shortcuts (navigating to and applying labels, archiving, etc.) ([more info](http://blog.persistent.info/2007/11/macros-for-new-version-of-gmail.html))
8 |
9 | # For the old version of Gmail
10 |
11 | * [Conversation Preview](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-conversation-preview.user.js): shows a preview bubble with a conversation contents while viewing the list of messages (i.e. without requiring navigation to another page) ([more info](http://blog.persistent.info/2005/08/gmail-conversation-preview-bubbles.html))
12 | * [Fixed Font Toggle](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-font-toggle.user.js): adds a font toggle button so that messages can be viewed in a fixed-width font ([more info](http://blog.persistent.info/2005/03/adding-persistent-searches-to-gmail.html))
13 | * [Label Colors](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-label-colors.user.js): allows labels to be colored so they stand out more ([more info](http://blog.persistent.info/2005/12/greasemonkey-christmas.html))
14 | * [Macros](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-macros.user.js): adds more keyboard shortcuts (for marking messages as read, moving them to the trash, applying labels, etc.) ([more info](http://blog.persistent.info/2005/12/greasemonkey-christmas.html), [even more info](http://blog.persistent.info/2006/11/greasemonkey-script-updates.html))
15 | * [Google Reader Integration](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-reader.user.js): adds a 'Feeds' links in the navigation area that opens up an embedded instance of Google Reader ([more info](http://blog.persistent.info/2006/10/google-reader-redux.html))
16 | * [Saved Searches](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-saved-searches.user.js): lets you save search queries so you can re-run them easily ([more info](http://blog.persistent.info/2005/03/adding-persistent-searches-to-gmail.html))
--------------------------------------------------------------------------------
/scripts/gmail-label-colors.user.js:
--------------------------------------------------------------------------------
1 | // Copyright 2006 Mihai Parparita. All Rights Reserved.
2 |
3 | // ==UserScript==
4 | // @name Gmail Label Colors
5 | // @namespace http://persistent.info/greasemonkey
6 | // @description Optionally colors label names (when they have a #color suffix)
7 | // @include http://mail.google.com/*
8 | // @include https://mail.google.com/*
9 | // ==/UserScript==
10 |
11 | // Constants
12 | const LABELS_INDEX = 8;
13 | const RULES = [
14 | ".ct .colored {-moz-border-radius: 8px; padding: 2px 4px 2px 4px; color: #fff;}"
15 | ];
16 |
17 | // These are actually dynamic, and Firefox seems to crash if we initialize them
18 | // inline, so they are set in overrideEscape()
19 | var NO_ESCAPE_START;
20 | var NO_ESCAPE_END;
21 |
22 | // All data received from the server goes through the function top.js.P. By
23 | // overriding it (but passing through data we get), we can be informed when
24 | // new sets conversations arrive and update the display accordingly.
25 | try {
26 | if (unsafeWindow.P && typeof(unsafeWindow.P) == "function") {
27 | var oldP = unsafeWindow.P;
28 |
29 | overrideEscape();
30 |
31 | unsafeWindow.P = function(iframe, data) {
32 | // Only override if it's a P(iframe, data) signature that we know about
33 | if (arguments.length == 2) {
34 | hookData(iframe, data);
35 | }
36 | oldP.apply(iframe, arguments);
37 | }
38 | }
39 | } catch (error) {
40 | // ignore;
41 | }
42 |
43 | function hookData(iframe, data) {
44 | var mode = data[0];
45 |
46 | switch (mode) {
47 | // conversation data
48 | case "t":
49 | for (var i = 1; i < data.length; i++) {
50 | var conversationData = data[i];
51 | var labels = conversationData[LABELS_INDEX];
52 |
53 | for (var j = 0; j < labels.length; j++) {
54 | var info = getLabelColorInfo(labels[j]);
55 |
56 | if (!info.color) continue;
57 |
58 | labels[j] =
59 | NO_ESCAPE_START +
60 | '' +
61 | info.name +
62 | '' +
63 | NO_ESCAPE_END;
64 | }
65 | }
66 | break;
67 | }
68 | }
69 |
70 | // Label names pass through an escaping function, which means that we can't
71 | // use HTML to color them. We override this function and allow the use of
72 | // markers within it turn off escaping as necessary.
73 | function overrideEscape() {
74 | // We want the no escape start/end markers to be non-deterministic, so that
75 | // an attacker can't embed arbitrary HTML into an evil message. The action token
76 | // seems like a good secret string.
77 | NO_ESCAPE_START = '';
78 | NO_ESCAPE_END = '';
79 |
80 | // First find the function (most escaping functions invoke the replace method
81 | // of the string object and have the string "<" in them
82 | for (var propName in unsafeWindow) {
83 | var propValue = unsafeWindow[propName];
84 |
85 | if (typeof(propValue) != "function") {
86 | continue;
87 | }
88 |
89 | var functionBody = propValue.toString();
90 |
91 | if (functionBody.indexOf("replace") != -1 &&
92 | functionBody.indexOf("<") != -1) {
93 |
94 | unsafeWindow[propName] = getEscapeClosure(propValue);
95 | break;
96 | }
97 | }
98 | }
99 |
100 | function getEscapeClosure(oldEscapeFunction) {
101 | return function(str) {
102 | var escaped = "";
103 | var start, end;
104 | while ((start = str.indexOf(NO_ESCAPE_START)) != -1) {
105 | escaped += oldEscapeFunction(str.substring(0, start));
106 |
107 | start += NO_ESCAPE_START.length;
108 | end = str.indexOf(NO_ESCAPE_END);
109 | escaped += str.substring(start, end);
110 | str = str.substring(end + NO_ESCAPE_END.length);
111 | }
112 | escaped += oldEscapeFunction(str);
113 |
114 | return escaped;
115 | }
116 | }
117 |
118 |
119 | function getLabelColorInfo(rawLabelName) {
120 | if (rawLabelName.indexOf(" #") != -1) {
121 | var split = rawLabelName.split(" #");
122 | return {name: split[0], color: split[1]};
123 | } else {
124 | return {name: rawLabelName};
125 | }
126 | }
127 |
128 | if (document.getElementById("tbd")) {
129 | insertStyles();
130 | }
131 |
132 | function insertStyles() {
133 | var styleNode = document.createElement("style");
134 |
135 | document.body.appendChild(styleNode);
136 |
137 | var styleSheet = document.styleSheets[document.styleSheets.length - 1];
138 |
139 | for (var i=0; i < RULES.length; i++) {
140 | styleSheet.insertRule(RULES[i], 0);
141 | }
142 | }
143 |
144 | function getCookie(name) {
145 | var re = new RegExp(name + "=([^;]+)");
146 | var value = re.exec(document.cookie);
147 | return (value != null) ? unescape(value[1]) : null;
148 | }
--------------------------------------------------------------------------------
/scripts/gmail-reader.user.js:
--------------------------------------------------------------------------------
1 | // Copyright 2006 Mihai Parparita. All Rights Reserved.
2 |
3 | // ==UserScript==
4 | // @name Gmail + Google Reader
5 | // @namespace http://persistent.info/
6 | // @description Embeds Google Reader into Gmail by adding a "Feeds" link
7 | // @include http://mail.google.com/*
8 | // @include https://mail.google.com/*
9 | // @include http://www.google.com/reader/*
10 | // ==/UserScript==
11 |
12 | var readerFrameNode = null;
13 | var feedsContainerNode = null;
14 | var hiddenNodes = [];
15 |
16 | const GMAIL_STYLES = [
17 | "#reader-frame {",
18 | " width: 100%;",
19 | " border: 0;",
20 | "}",
21 |
22 | ".reader-embedded #ft {",
23 | "display: none",
24 | "}",
25 |
26 | // Make currently selected item appear normal
27 | ".reader-embedded table.cv * {",
28 | " background: #fff;",
29 | " font-weight: normal",
30 | "}",
31 |
32 | // Make the feeds link appear selected
33 | ".reader-embedded #feeds-container {",
34 | " background: #c3d9ff;",
35 | " -moz-border-radius: 5px 0 0 5px;",
36 | " font-weight: bold;",
37 | " color: #00c;",
38 | "}",
39 | ].join("\n");
40 |
41 | const READER_STYLES = [
42 | "body {",
43 | " background: #fff",
44 | "}",
45 |
46 | "#nav,",
47 | "#logo-container,",
48 | "#global-info,",
49 | "#viewer-header {",
50 | " display: none !important;",
51 | "}",
52 | "",
53 | "#main {",
54 | " margin-top: 0;",
55 | "}",
56 | "",
57 | "#chrome {",
58 | " margin-left: 0;",
59 | "}"
60 | ].join("\n");
61 |
62 | const READER_UNREAD_COUNT_URL =
63 | "http://www.google.com/reader/api/0/unread-count?" +
64 | "all=true&output=json&client=gm";
65 |
66 | const READER_LIST_VIEW_URL =
67 | "http://www.google.com/reader/view/user/-/state/com.google/reading-list?" +
68 | "gmail-embed=true&view=list";
69 |
70 | if (document.location.hostname == "mail.google.com") {
71 | injectGmail();
72 | } else if (document.location.hostname == "www.google.com" &&
73 | document.location.search.indexOf("gmail-embed") != -1) {
74 | injectReader();
75 | }
76 |
77 | function injectGmail() {
78 | if (!getNode("nds") || !getNode("ds_inbox")) return;
79 |
80 | GM_addStyle(GMAIL_STYLES);
81 |
82 | var feedsNode = newNode("span");
83 | feedsNode.className = "lk";
84 | feedsNode.innerHTML =
85 | 'Feeds ' +
86 | '';
87 | feedsNode.onclick = showReaderFrame;
88 |
89 | feedsContainerNode = newNode("div");
90 | feedsContainerNode.className = "nl";
91 | feedsContainerNode.id = "feeds-container";
92 | feedsContainerNode.appendChild(feedsNode);
93 |
94 | var navNode = getNode("nds");
95 |
96 | navNode.insertBefore(feedsContainerNode, navNode.childNodes[2]);
97 |
98 | window.addEventListener("resize", resizeReaderFrame, false);
99 |
100 | updateUnreadCount();
101 |
102 | window.setInterval(updateUnreadCount, 5 * 60 * 1000);
103 |
104 | window.setInterval(checkFeedsParent, 1000);
105 | }
106 |
107 | function checkFeedsParent() {
108 | var navNode = getNode("nds");
109 |
110 | if (feedsContainerNode.parentNode != navNode) {
111 | navNode.insertBefore(feedsContainerNode, navNode.childNodes[2]);
112 | }
113 | }
114 |
115 | function updateUnreadCount() {
116 | var unreadCountNode = getNode("reader-unread-count");
117 |
118 | if (!unreadCountNode) return;
119 |
120 | GM_xmlhttpRequest({
121 | method: "GET",
122 | url: READER_UNREAD_COUNT_URL,
123 | onload: function(details) {
124 | var data = eval("(" + details.responseText + ")");
125 | var isUnread = false;
126 |
127 | for (var i = 0, unreadCountPair;
128 | unreadCountPair = data.unreadcounts[i];
129 | i++) {
130 | if (unreadCountPair.id.indexOf("reading-list") != -1) {
131 | var count = unreadCountPair.count;
132 | if (count == 0) break;
133 |
134 | unreadCountNode.innerHTML =
135 | " (" + count + (count == data.max ? "+" : "") + ") ";
136 | isUnread = true;
137 | break;
138 | }
139 | }
140 |
141 | if (!isUnread) {
142 | unreadCountNode.innerHTML = "";
143 | }
144 | }
145 | });
146 | }
147 |
148 | function resizeReaderFrame() {
149 | if (!readerFrameNode) return;
150 |
151 | readerFrameNode.style.height =
152 | (window.innerHeight - readerFrameNode.offsetTop) + "px";
153 | }
154 |
155 | function showReaderFrame(event) {
156 | var container = getNode("co");
157 |
158 | addClass(document.body, "reader-embedded");
159 |
160 | hiddenNodes = [];
161 |
162 | for (var i = container.firstChild; i; i = i.nextSibling) {
163 | hiddenNodes.push(i);
164 | i.style.display = "none";
165 | }
166 |
167 | readerFrameNode = newNode("iframe");
168 | readerFrameNode.src = READER_LIST_VIEW_URL;
169 | readerFrameNode.id = "reader-frame";
170 |
171 | container.appendChild(readerFrameNode);
172 |
173 | container.parentNode.style.paddingRight = "0";
174 | container.parentNode.style.paddingBottom = "0";
175 |
176 | resizeReaderFrame();
177 |
178 | // Make clicks outside the content area hide it
179 | getNode("nav").addEventListener("click", hideReaderFrame, false);
180 |
181 | // Since we're in a child of the "nav" element, the above handler will get
182 | // triggered immediately unless we stop this event from propagating
183 | event.stopPropagation();
184 |
185 | return false;
186 | }
187 |
188 | function hideReaderFrame() {
189 | var container = getNode("co");
190 |
191 | container.removeChild(readerFrameNode);
192 | readerFrameNode = null;
193 |
194 | for (var i=0; i < hiddenNodes.length; i++) {
195 | hiddenNodes[i].style.display = "";
196 | }
197 | getNode("nav").removeEventListener("click", hideReaderFrame, false);
198 |
199 | removeClass(document.body, "reader-embedded");
200 |
201 | container.parentNode.style.paddingRight = "1ex";
202 | container.parentNode.style.paddingBottom = "1ex";
203 |
204 | return true;
205 | }
206 |
207 | function injectReader() {
208 | GM_addStyle(READER_STYLES);
209 | }
210 |
211 | // Shorthand
212 | function newNode(type) {return unsafeWindow.document.createElement(type);}
213 | function newText(text) {return unsafeWindow.document.createTextNode(text);}
214 | function getNode(id) {return unsafeWindow.document.getElementById(id);}
215 |
216 | function hasClass(node, className) {
217 | return className in getClassMap(node);
218 | }
219 |
220 | function addClass(node, className) {
221 | if (hasClass(node, className)) return;
222 |
223 | node.className += " " + className;
224 | }
225 |
226 | function removeClass(node, className) {
227 | var classMap = getClassMap(node);
228 |
229 | if (!(className in classMap)) return;
230 |
231 | delete classMap[className];
232 | var newClassList = [];
233 |
234 | for (var className in classMap) {
235 | newClassList.push(className);
236 | }
237 |
238 | node.className = newClassList.join(" ");
239 | }
240 |
241 | function getClassMap(node) {
242 | var classMap = {};
243 | var classNames = node.className.split(/\s+/);
244 |
245 | for (var i = 0; i < classNames.length; i++) {
246 | classMap[classNames[i]] = true;
247 | }
248 |
249 | return classMap;
250 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
--------------------------------------------------------------------------------
/scripts/gmail-new-macros.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Gmail Macros (New)
3 | // @namespace http://persistent.info
4 | // @include http://mail.google.com/*
5 | // @include https://mail.google.com/*
6 | // ==/UserScript==
7 |
8 | window.addEventListener('load', function() {
9 | if (unsafeWindow.gmonkey) {
10 | unsafeWindow.gmonkey.load('1.0', init)
11 | }
12 | }, true);
13 |
14 | var UNREAD_COUNT_RE = /\s+\(\d+\)?$/;
15 |
16 | var MORE_ACTIONS_MENU_HEADER_CLASS = "QOD9Ec";
17 | var MORE_ACTIONS_MENU_BODY_CLASS = "Sn99bd";
18 | var MORE_ACTIONS_MENU_ITEM_CLASS = "SenFne";
19 |
20 | var LABEL_ITEM_CLASS_NAME = "yyT6sf";
21 |
22 | var MARK_AS_READ_ACTION = "1";
23 | var ARCHIVE_ACTION = "7";
24 | var ADD_LABEL_ACTION = "12";
25 | var REMOVE_LABEL_ACTION = "13";
26 |
27 | // Map from nav pane names to location names
28 | var SPECIAL_LABELS = {
29 | "Inbox": "inbox",
30 | "Starred": "starred",
31 | "Chats": "chats",
32 | "Sent Mail": "sent",
33 | "Drafts": "drafts",
34 | "All Mail": "all",
35 | "Spam": "spam",
36 | "Trash": "trash"
37 | }
38 |
39 | const LABEL_ACTIONS = {
40 | // g: go to label
41 | 71: {
42 | label: "Go to label",
43 | func: function(labelName) {
44 | if (labelName in SPECIAL_LABELS) {
45 | top.location.hash = "#" + SPECIAL_LABELS[labelName];
46 | } else {
47 | top.location.hash = "#label/" + encodeURIComponent(labelName);
48 | }
49 | }
50 | },
51 | // l: apply label
52 | 76: {
53 | label: "Apply label",
54 | func: function (labelName) {
55 | clickMoreActionsMenuItem(labelName, ADD_LABEL_ACTION);
56 | },
57 | },
58 | // b: remove label
59 | 66: {
60 | label: "Remove label",
61 | func: function (labelName) {
62 | clickMoreActionsMenuItem(labelName, REMOVE_LABEL_ACTION);
63 | }
64 | }
65 | };
66 |
67 | const ACTIONS = {
68 | // d: archive and mark as read, i.e. discard
69 | 68: function() {
70 | clickMoreActionsMenuItem("Mark as read", MARK_AS_READ_ACTION);
71 |
72 | // Wait for the mark as read action to complete
73 | window.setTimeout(function() {
74 | var archiveButton = getFirstVisibleNode(
75 | evalXPath(".//button[@act='" + ARCHIVE_ACTION + "']", getDoc().body));
76 |
77 | if (archiveButton) {
78 | simulateClick(archiveButton, "click");
79 | } else {
80 | clickMoreActionsMenuItem("Archive", ARCHIVE_ACTION);
81 | }
82 | }, 500);
83 | },
84 | // f: focus (only show unread and inbox messages)
85 | 70: function() {
86 | // Can only focus when in threadlist views
87 | if (gmail.getActiveViewType() != 'tl') return;
88 |
89 | var loc = top.location.hash;
90 | if (loc.length <= 1) return;
91 | loc = loc.substring(1);
92 |
93 | var search = getSearchForLocation(loc);
94 |
95 | if (search === null) {
96 | return;
97 | }
98 |
99 | search += " {in:inbox is:starred is:unread} -is:muted";
100 |
101 | top.location.hash = "#search/" + search;
102 | }
103 | };
104 |
105 | var LOC_TO_SEARCH = {
106 | "inbox": "in:inbox",
107 | "starred": "is:starred",
108 | "chats": "is:chat",
109 | "sent": "from:me",
110 | "drafts": "is:draft",
111 | "all": "",
112 | "spam": "in:spam",
113 | "trash": "in:trash"
114 | };
115 |
116 | var LABEL_PREFIX = "label/";
117 |
118 | function getSearchForLocation(loc) {
119 | if (loc in LOC_TO_SEARCH) {
120 | return LOC_TO_SEARCH[loc];
121 | }
122 |
123 | if (loc.indexOf(LABEL_PREFIX) == 0) {
124 | var labelName = loc.substring(LABEL_PREFIX.length);
125 |
126 | // Normalize spaces to dashes, since that's what Gmail wants for searches
127 | labelName = labelName.replace(/\+/g, "-");
128 |
129 | return "label:" + labelName;
130 | }
131 |
132 | return null;
133 | }
134 |
135 | // TODO(mihaip): too many global variables, use objects
136 | var banner = null;
137 | var gmail = null;
138 |
139 | var labelInput = null;
140 | var activeLabelAction = null;
141 | var lastPrefix = null;
142 | var selLabelIndex = null;
143 |
144 | function getDoc() {
145 | return gmail.getNavPaneElement().ownerDocument;
146 | }
147 |
148 | function newNode(tagName) {
149 | return getDoc().createElement(tagName);
150 | }
151 |
152 | function getNode(id) {
153 | return getDoc().getElementById(id);
154 | }
155 |
156 | function getFirstVisibleNode(nodes) {
157 | for (var i = 0, node; node = nodes[i]; i++) {
158 | if (node.offsetHeight) return node;
159 | }
160 |
161 | return null;
162 | }
163 |
164 | function simulateClick(node, eventType) {
165 | var event = node.ownerDocument.createEvent("MouseEvents");
166 | event.initMouseEvent(eventType,
167 | true, // can bubble
168 | true, // cancellable
169 | node.ownerDocument.defaultView,
170 | 1, // clicks
171 | 50, 50, // screen coordinates
172 | 50, 50, // client coordinates
173 | false, false, false, false, // control/alt/shift/meta
174 | 0, // button,
175 | node);
176 |
177 | node.dispatchEvent(event);
178 | }
179 |
180 | function clickMoreActionsMenuItem(menuItemText, menuItemAction) {
181 | var moreActionsMenu = getFirstVisibleNode(getNodesByTagNameAndClass(
182 | getDoc().body, "div", MORE_ACTIONS_MENU_HEADER_CLASS));
183 |
184 | if (!moreActionsMenu) {
185 | alert("Couldn't find the menu header node");
186 | return;
187 | }
188 |
189 | simulateClick(moreActionsMenu, "mousedown");
190 |
191 | var menuBodyNodes = getNodesByTagNameAndClass(
192 | getDoc().body, "div", MORE_ACTIONS_MENU_BODY_CLASS);
193 | var menuBodyNode = getFirstVisibleNode(menuBodyNodes);
194 |
195 | if (!menuBodyNode) {
196 | alert("Couldn't find the menu body node");
197 | return;
198 | }
199 |
200 | var menuItemNodes = getNodesByTagNameAndClass(
201 | menuBodyNode, "div", MORE_ACTIONS_MENU_ITEM_CLASS);
202 |
203 | for (var i = 0; menuItemNode = menuItemNodes[i]; i++) {
204 | if (menuItemNode.textContent == menuItemText &&
205 | menuItemNode.getAttribute("act") == menuItemAction) {
206 | simulateClick(menuItemNode, "mouseup");
207 | return;
208 | }
209 | }
210 |
211 | alert("Couldn't find the menu item node '" + menuItemText + "'");
212 | }
213 |
214 | function init(g) {
215 | gmail = g;
216 | banner = new Banner();
217 |
218 | getDoc().defaultView.addEventListener('keydown', keyHandler, false);
219 | }
220 |
221 | function keyHandler(event) {
222 | // Apparently we still see Firefox shortcuts like control-T for a new tab -
223 | // checking for modifiers lets us ignore those
224 | if (event.altKey || event.ctrlKey || event.metaKey) return;
225 |
226 | // We also don't want to interfere with regular user typing
227 | if (event.target && event.target.nodeName) {
228 | var targetNodeName = event.target.nodeName.toLowerCase();
229 | if (targetNodeName == "textarea" ||
230 | (targetNodeName == "input" && event.target.type &&
231 | (event.target.type.toLowerCase() == "text" ||
232 | event.target.type.toLowerCase() == "file"))) {
233 | return;
234 | }
235 | }
236 |
237 | var k = event.keyCode;
238 |
239 | if (k in LABEL_ACTIONS) {
240 | if (activeLabelAction) {
241 | endLabelAction();
242 | return
243 | } else {
244 | activeLabelAction = LABEL_ACTIONS[k];
245 | beginLabelAction();
246 | return;
247 | }
248 | }
249 |
250 | if (k in ACTIONS) {
251 | ACTIONS[k]();
252 | return;
253 | }
254 |
255 | return;
256 | }
257 |
258 | function beginLabelAction() {
259 | // TODO(mihaip): make sure the labels nav pane is open
260 |
261 | banner.show();
262 | banner.setFooter(activeLabelAction.label);
263 |
264 | lastPrefix = null;
265 | selLabelIndex = 0;
266 | dispatchedActionTimeout = null;
267 |
268 | labelInput = makeLabelInput();
269 | labelInput.addEventListener("keyup", updateLabelAction, false);
270 | // we want escape, clicks, etc. to cancel, which seems to be equivalent to the
271 | // field losing focus
272 | labelInput.addEventListener("blur", endLabelAction, false);
273 | }
274 |
275 | function makeLabelInput() {
276 | labelInput = newNode("input");
277 | labelInput.type = "text";
278 | labelInput.setAttribute("autocomplete", "off");
279 | with (labelInput.style) {
280 | position = "fixed"; // We need to use fixed positioning since we have to ensure
281 | // that the input is not scrolled out of view (since
282 | // Gecko will scroll for us if it is).
283 | top = "0";
284 | left = "-300px";
285 | width = "200px";
286 | height = "20px";
287 | zIndex = "1000";
288 | }
289 |
290 | getDoc().body.appendChild(labelInput);
291 | labelInput.focus();
292 | labelInput.value = "";
293 |
294 | return labelInput;
295 | }
296 |
297 | function endLabelAction() {
298 | if (dispatchedActionTimeout) return;
299 |
300 | // TODO(mihaip): re-close label box if necessary
301 |
302 | banner.hide();
303 |
304 | if (labelInput) {
305 | labelInput.parentNode.removeChild(labelInput);
306 | labelInput = null;
307 | }
308 |
309 | activeLabelAction = null;
310 | }
311 |
312 | function updateLabelAction(event) {
313 | // We've already dispatched the action, the user is just typing away
314 | if (dispatchedActionTimeout) return;
315 |
316 | var labels = getLabels();
317 | var selectedLabels = [];
318 |
319 | // We need to skip the label shortcut that got us here
320 | var labelPrefix = labelInput.value.substring(1).toLowerCase();
321 |
322 | // We always want to reset the cursor position to the end of the text
323 | // field, since some of the keys that we support (arrows) would
324 | // otherwise change it
325 | labelInput.selectionStart = labelInput.selectionEnd = labelPrefix.length + 1;
326 |
327 | if (labelPrefix.length == 0) {
328 | banner.update("");
329 | return;
330 | }
331 |
332 | for (var i = 0; i < labels.length; i++) {
333 | label = labels[i];
334 |
335 | if (label.toLowerCase().indexOf(labelPrefix) == 0) {
336 | selectedLabels.push(label);
337 | }
338 | }
339 |
340 | if (labelPrefix != lastPrefix) {
341 | lastPrefix = labelPrefix;
342 | selLabelIndex = 0;
343 | }
344 |
345 | if (selectedLabels.length == 0) {
346 | banner.update(labelPrefix);
347 | return;
348 | }
349 |
350 | if (event.keyCode == 13 || selectedLabels.length == 1) {
351 | var selectedLabelName = selectedLabels[selLabelIndex];
352 |
353 | // Tell the user what we picked
354 | banner.update(selectedLabelName);
355 |
356 | // Invoke the action straight away, but keep the banner up so the user can
357 | // see what was picked, and so that extra typing is caught.
358 | activeLabelAction.func(selectedLabelName);
359 | dispatchedActionTimeout = window.setTimeout(function() {
360 | dispatchedActionTimeout = null;
361 | endLabelAction()
362 | }, 500);
363 | return;
364 | } else if (event.keyCode == 40) { // down
365 | selLabelIndex = (selLabelIndex + 1) % selectedLabels.length;
366 | } else if (event.keyCode == 38) { // up
367 | selLabelIndex = (selLabelIndex + selectedLabels.length - 1) %
368 | selectedLabels.length;
369 | }
370 |
371 | var selectedLabelName = selectedLabels[selLabelIndex];
372 |
373 | var highlightedSelectedLabelName = selectedLabelName.replace(
374 | new RegExp("(" + labelPrefix + ")", "i"), "$1");
375 | var labelPosition = " (" +
376 | (selLabelIndex + 1) + "/" + selectedLabels.length + ")";
377 |
378 | banner.update(highlightedSelectedLabelName + labelPosition);
379 | }
380 |
381 | function getLabels() {
382 | var navPaneNode = gmail.getNavPaneElement();
383 |
384 | var labelNodes = getNodesByTagNameAndClass(
385 | navPaneNode, "div", LABEL_ITEM_CLASS_NAME);
386 |
387 | var labels = [];
388 |
389 | for (var i = 0, labelNode; labelNode = labelNodes[i]; i++) {
390 | var labelName = labelNode.textContent.replace(UNREAD_COUNT_RE, "");
391 |
392 | labels.push(labelName);
393 | }
394 |
395 | return labels;
396 | }
397 |
398 | function evalXPath(expression, rootNode) {
399 | try {
400 | var xpathIterator = rootNode.ownerDocument.evaluate(
401 | expression,
402 | rootNode,
403 | null, // no namespace resolver
404 | XPathResult.ORDERED_NODE_ITERATOR_TYPE,
405 | null); // no existing results
406 | } catch (err) {
407 | GM_log("Error when evaluating XPath expression '" + expression + "'" +
408 | ": " + err);
409 | return null;
410 | }
411 | var results = [];
412 |
413 | // Convert result to JS array
414 | for (var xpathNode = xpathIterator.iterateNext();
415 | xpathNode;
416 | xpathNode = xpathIterator.iterateNext()) {
417 | results.push(xpathNode);
418 | }
419 |
420 | return results;
421 | }
422 |
423 | function getNodesByTagNameAndClass(rootNode, tagName, className) {
424 | var expression =
425 | ".//" + tagName +
426 | "[contains(concat(' ', @class, ' '), ' " + className + " ')]";
427 |
428 | return evalXPath(expression, rootNode);
429 | }
430 |
431 | function Banner() {
432 | function getNodeSet() {
433 | var boxNode = newNode("div");
434 | boxNode.className = "banner";
435 | with (boxNode.style) {
436 | display = "none";
437 | position = "fixed";
438 | left = "10%";
439 | margin = "0 10% 0 10%";
440 | width = "60%";
441 | textAlign = "center";
442 | MozBorderRadius = "10px";
443 | padding = "10px";
444 | color = "#fff";
445 | }
446 |
447 | var messageNode = newNode("div");
448 | with (messageNode.style) {
449 | fontSize = "24px";
450 | fontWeight = "bold";
451 | fontFamily = "Lucida Grande, Trebuchet MS, sans-serif";
452 | margin = "0 0 10px 0";
453 | }
454 | boxNode.appendChild(messageNode);
455 |
456 | var taglineNode = newNode("div");
457 | with (taglineNode.style) {
458 | fontSize = "13px";
459 | margin = "0";
460 | position = "absolute";
461 | right = "0.2em";
462 | bottom = "0";
463 | MozOpacity = "0.5";
464 | }
465 | taglineNode.innerHTML = 'LabelSelector9001';
466 | boxNode.appendChild(taglineNode);
467 |
468 | var footerNode = newNode("div");
469 | with (footerNode.style) {
470 | fontSize = "13px";
471 | }
472 | boxNode.appendChild(footerNode);
473 |
474 | return boxNode;
475 | }
476 |
477 | this.backgroundNode = getNodeSet();
478 | this.backgroundNode.style.background = "#000";
479 | this.backgroundNode.style.MozOpacity = "0.70";
480 | this.backgroundNode.style.zIndex = 100;
481 | for (var child = this.backgroundNode.firstChild;
482 | child;
483 | child = child.nextSibling) {
484 | child.style.visibility = "hidden";
485 | }
486 |
487 | this.foregroundNode = getNodeSet();
488 | this.foregroundNode.style.zIndex = 101;
489 | }
490 |
491 | Banner.prototype.hide = function() {
492 | this.backgroundNode.style.display =
493 | this.foregroundNode.style.display = "none";
494 | }
495 |
496 | Banner.prototype.show = function(opt_isBottomAnchored) {
497 | this.update("");
498 | getDoc().body.appendChild(this.backgroundNode);
499 | getDoc().body.appendChild(this.foregroundNode);
500 |
501 | this.backgroundNode.style.bottom = this.foregroundNode.style.bottom =
502 | opt_isBottomAnchored ? "10%" : "";
503 | this.backgroundNode.style.top = this.foregroundNode.style.top =
504 | opt_isBottomAnchored ? "" : "50%";
505 |
506 | this.backgroundNode.style.display =
507 | this.foregroundNode.style.display = "block";
508 | }
509 |
510 | Banner.prototype.update = function(message) {
511 | if (message.length) {
512 | this.backgroundNode.firstChild.style.display =
513 | this.foregroundNode.firstChild.style.display = "inline";
514 | } else {
515 | this.backgroundNode.firstChild.style.display =
516 | this.foregroundNode.firstChild.style.display = "none";
517 | }
518 | this.backgroundNode.firstChild.innerHTML =
519 | this.foregroundNode.firstChild.innerHTML = message;
520 | }
521 |
522 | Banner.prototype.setFooter = function(text) {
523 | this.backgroundNode.lastChild.innerHTML =
524 | this.foregroundNode.lastChild.innerHTML = text;
525 | }
526 |
--------------------------------------------------------------------------------
/scripts/gmail-macros.user.js:
--------------------------------------------------------------------------------
1 | // Copyright 2006 Mihai Parparita. All Rights Reserved.
2 |
3 | // ==UserScript==
4 | // @name Gmail Macros
5 | // @namespace http://persistent.info/greasemonkey
6 | // @description Extra (customizable) keyboard shortcuts and macros.
7 | // @include http://mail.google.com/*
8 | // @include https://mail.google.com/*
9 | // ==/UserScript==
10 |
11 | // Constants
12 |
13 | const LABEL_PREFIX = "sc_";
14 | const SELECT_PREFIX = "sl_";
15 | const SAVED_SEARCH_PREFIX = "savedsearch_";
16 |
17 | // Maps human readable names to DOM node IDs
18 | const SPECIAL_LABELS = {
19 | "Inbox": "ds_inbox",
20 | "Starred": "ds_starred",
21 | "Chats": "ds_chats",
22 | "Sent Mail": "ds_sent",
23 | "Drafts": "ds_drafts",
24 | "All Mail": "ds_all",
25 | "Spam": "ds_spam",
26 | "Trash": "ds_trash",
27 | "Contacts": "cont"
28 | };
29 |
30 | // Command Names
31 | const MARK_AS_READ = "rd";
32 | const MARK_AS_UNREAD = "ur";
33 |
34 | const ARCHIVE = "rc_^i";
35 | const MOVE_TO_INBOX = "ib";
36 | const ADD_STAR = "st";
37 | const REMOVE_STAR = "xst";
38 |
39 | const APPLY_LABEL = "ac_"; // Followed by label name
40 | const REMOVE_LABEL = "rc_"; // Followed by label name
41 |
42 | const MOVE_TO_TRASH = "tr";
43 | const DELETE_FOREVER = "dl"; // Only works when in trash and spam views
44 |
45 | const REPORT_SPAM = "sp";
46 | const NOT_SPAM = "us";
47 |
48 | const HANDLERS_TABLE = {
49 | 68: [MARK_AS_READ, ARCHIVE], // D: Discard
50 | 69: [ARCHIVE], // E: always archivE (Y's context-dependent behavior is annoying)
51 | 82: [MARK_AS_READ], // R: mark as Read
52 | 84: [MOVE_TO_TRASH],// T: move to Trash
53 | 90: [MARK_AS_UNREAD] // Z: mark as Unread (undo read similar to Ctrl+Z)
54 | };
55 |
56 | const LABEL_ACTIONS = {
57 | // g: go to label
58 | 71: function(labelName) {
59 | var labelDiv = getLabelNode(labelName);
60 |
61 | var eventType = labelName == "Contacts" ? "click" : "mousedown";
62 |
63 | simulateClick(labelDiv, eventType);
64 | },
65 | // l: apply label
66 | 76: function (labelName) {
67 | // we don't do special labels (there's other commands, like "archive" for
68 | // that)
69 | if (labelName in SPECIAL_LABELS) {
70 | return;
71 | }
72 |
73 | runCommands([APPLY_LABEL + labelName]);
74 | },
75 | // b: remove label
76 | 66: function (labelName) {
77 | // we don't do special labels (there's other commands, like "archive" for
78 | // that)
79 | if (labelName in SPECIAL_LABELS) {
80 | return;
81 | }
82 |
83 | runCommands([REMOVE_LABEL + labelName]);
84 | }
85 | };
86 |
87 | const SELECT_KEY_VALUES = {
88 | 65: ['a','All'],
89 | 78: ['n','None'],
90 | 82: ['r','Read'],
91 | 83: ['s','Starred'],
92 | 84: ['t','Unstarred'],
93 | 85: ['u','Unread']
94 | };
95 |
96 | const SELECT_ACTIONS = {
97 | // shift-x: select
98 | 88: function(selectionName) {
99 | var selectDiv = getNode(SELECT_PREFIX + selectionName);
100 | simulateClick(selectDiv, "mousedown");
101 | },
102 | // h: show help
103 | 72: function() {
104 | banner.show(true);
105 | banner.update(getHelpHtml());
106 | }
107 | };
108 |
109 | const SIMPLE_ACTIONS = {
110 | // o: expand/collapse all
111 | 79: function(selectionName) {
112 | if(getNode("ec")){
113 | simulateClick(getNode("ec"), "mousedown");
114 | }
115 | if(getNode("ind")){
116 | simulateClick(getNode("ind"), "mousedown");
117 | }
118 | }
119 | };
120 |
121 | const BUILTIN_KEYS_HELP = {
122 | "C*" : "Compose",
123 | "/" : "Search",
124 | "Q" : "Quick contacts",
125 | "J/K" : "Move to an older/newer conversation",
126 | "N/P" : "Next/Previous message",
127 | "<Enter>" : "Open*, expand/collapse, press button",
128 | "U" : "Return to the conversation list",
129 | "Y" : "Archive/remove from current view",
130 | "X" : "Select a conversation",
131 | "S" : "Star a message or conversation",
132 | "!" : "Report Spam!",
133 | "R*" : "Reply",
134 | "A*" : "Reply All",
135 | "F*" : "Forward"
136 | };
137 |
138 | const ADDED_KEYS_HELP = {
139 | "H" : "What are the keyboard commands?",
140 | "T" : "Trash conversation(s)",
141 | "E" : "ArchivE conversations(s) (always)",
142 | "R" : "Mark conversation(s) as Read",
143 | "Z" : "Mark conversation(s) as unread (vs. Ctrl+Z undo)",
144 | "D" : "Discard (read & archive) conversation(s)",
145 | "O" : "Expand/collapse all messages in a conversation",
146 |
147 | " " : " ",
148 | "V" : "PreView a conversation
(requires Gmail Conversation Preview)",
149 | " " : " ",
150 |
151 | "G+label" : "Go to label (including inbox/starred/trash/etc.)",
152 | "L+label" : "Label conversation(s) as label",
153 | "B+label" : "Remove label from conversation(s)",
154 |
155 | "<Shift>-X+key" : "Select " +
156 | "A - All, " +
157 | "N - None, " +
158 | "R - Read,
" +
159 | "U - Unread, " +
160 | "S - Starred, " +
161 | "T - UnsTarred"
162 | };
163 |
164 | // Utility functions
165 | function simulateClick(node, eventType) {
166 | var event = node.ownerDocument.createEvent("MouseEvents");
167 | event.initMouseEvent("mousedown",
168 | true, // can bubble
169 | true, // cancellable
170 | window,
171 | 1, // clicks
172 | 50, 50, // screen coordinates
173 | 50, 50, // client coordinates
174 | false, false, false, false, // control/alt/shift/meta
175 | 0, // button,
176 | node);
177 | node.dispatchEvent(event);
178 | }
179 |
180 | function getHelpHtml() {
181 | var html =
182 | '
| Standard | Extended | ' + 188 | '" + key + " | " + BUILTIN_KEYS_HELP[key] + " | "); 193 | } 194 | 195 | var added = []; 196 | for (var key in ADDED_KEYS_HELP) { 197 | added.push("" + key + " | " + ADDED_KEYS_HELP[key] + " | "); 198 | } 199 | 200 | for(var i = 0; i < base.length; i++) { 201 | html += "
|---|---|---|---|
| ' + 207 | '* Hold <Shift> for action in a new window' + 208 | ' | ' + 209 | '|||
Use operators ' +
392 | 'to specify queries. today, yesterday and oneweekago ' +
393 | 'are also supported as values for the before: and after: ' +
394 | 'operators. ' +
395 | 'Prefix your search query with ++ ' +
396 | 'to make a search that will ADD your query to the current query. ' +
397 | 'Delete an item\'s query to remove it.