// Copyright 2006 Mihai Parparita. All Rights Reserved. // ==UserScript== // @name Gmail Saved Searches // @namespace http://www.google.com/~mihaip // @description Adds saved and recent seaches. // @include http://mail.google.com/* // @include https://mail.google.com/* // ==/UserScript== // Utility functions function getObjectMethodClosure(object, method) { return function() { return object[method].apply(object, arguments); } } function getDateString(date) { return date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate(); } function getCookie(name) { name = getNamespacedName(name); if (GM_getValue(name)) { return GM_getValue(name); } var re = new RegExp(name + "=([^;]+)"); var value = re.exec(document.cookie); return (value != null) ? unescape(value[1]) : null; } function setCookie(name, value) { name = getNamespacedName(name); GM_setValue(name, value); } var email = null; function getNamespacedName(name) { if (email == null) { var settingsNode = getNode("prf_g"); var emailNode = settingsNode; do { emailNode = emailNode.previousSibling; } while (!emailNode.innerHTML || emailNode.innerHTML.indexOf("@") == -1); email = encodeURIComponent(emailNode.innerHTML); } return email + "-" + name; } // Shorthand var newNode = getObjectMethodClosure(unsafeWindow.document, "createElement"); var newText = getObjectMethodClosure(unsafeWindow.document, "createTextNode"); var getNode = getObjectMethodClosure(unsafeWindow.document, "getElementById"); // Contants const RULES = new Array( // Block in sidebar ".searchesBlock {-moz-border-radius: 5px; background: #fad163; margin: 10px 7px 0 0; padding: 3px;}", ".refreshButton {display: block; cursor: pointer; float: right; margin-top: -2px;}", ".searchesBlockList {padding-left: 5px; background: white; overflow: hidden; display: none;}", ".searchesBlockList h5 {margin: 0 0 1px -4px; color: #999; font-size: 12px; font-weight: bold;}", ".listItem {color: #ca9c22;}", ".editLink {background: white; text-align: right; color: #ca9c22; padding: 2px 0px 5px 0;}", // Edit page ".searchesContainer {-moz-border-radius: 10px; background: #fad163; padding: 10px;}", ".innerContainer {background: #fff7d7; text-align: left; padding: 10px;}", ".buttonContainer {text-align: center;}", ".searchesList {width: 100%;}", ".searchesList th {text-align: left; font-size: 90%;}", ".searchesList td {padding: 10px 0 10px 0; vertical-align: bottom;}", ".searchesList td.divider {background: #fad163; height: 3px; padding: 0;}", ".editItem {font-size: 80%;}", ".labelCell {width: 210px;}", ".labelCell input {width: 200px;}", ".cancelButton {margin-right: 5px;}", ".editCell {}", ".editCell input {width: 100%}", ".saveButton {margin-left: 5px; font-weight: bold;}" ); const UP_TRIANGLE_IMAGE = "data:image/gif;base64,R0lGODlhCwALAKEAAP///wAAAA4" + "ODv///yH5BAEAAAMALAAAAAALAAsAAAITnI+pGmsBF5xp2mPzmCJHB4ZJAQA7"; const DEFAULT_SEARCHES = { "to:me {in:inbox is:unread}": "TODO", "has:attachment": "Attachments", "after:oneweekago": "Last Week", "label:^g is:unread": "Muted but unread", "++ {is:unread in:inbox is:starred}": "Add Focus" }; const SEARCHES_COOKIE = "PersistentSearches"; const SEARCHES_COLLAPSED_COOKIE = "PersistentSearchesCollapsedCookie"; const ONE_DAY = 24 * 60 * 60 * 1000; const SAVED_SEARCH_PREFIX = "savedsearch_"; const KEY_PREFIX = "gmailss"; const POSITION = KEY_PREFIX + "pos"; const RECENT_NUM = KEY_PREFIX + "numrecent"; const RECENT_POS = KEY_PREFIX + "recentpos"; const NEW_VALUE = "NEW"; // Globals var styleSheet = null; var searches = new Array(); var recentSearches = new Array(); var searchesBlock = null; var searchesBlockHeader = null; var searchesBlockList = null; var recentSearchesBlockList = null; var editLink = null; var hiddenNodes = null; var searchesContainer = null; var searchesList = null; function initializePersistentSearches() { var labelsBlock = getNode("nb_0"); if (!labelsBlock) { return; } searchesBlock = newNode("div"); searchesBlock.id = "nb_9"; searchesBlock.className = "searchesBlock"; // header searchesBlockHeader = newNode("div"); searchesBlockHeader.className = "s h"; searchesBlock.appendChild(searchesBlockHeader); searchesBlockHeader.triangleImage = newNode("img"); searchesBlockHeader.triangleImage.src = "/mail/images/opentriangle.gif"; searchesBlockHeader.triangleImage.width = 11; searchesBlockHeader.triangleImage.height = 11; searchesBlockHeader.triangleImage.addEventListener("click", togglePersistentSearches, false); searchesBlockHeader.appendChild(searchesBlockHeader.triangleImage); var searchesText = newNode("span"); searchesText.appendChild(newText(" Searches")); searchesText.addEventListener("click", togglePersistentSearches, false); searchesBlockHeader.appendChild(searchesText); // recent searches list recentSearchesBlockList = newNode("div"); recentSearchesBlockList.className = "searchesBlockList"; recentSearchesBlockList.appendChild(newNode("h5")).appendChild(newText("Recent")); // saved searches list searchesBlockList = newNode("div"); searchesBlockList.className = "searchesBlockList"; searchesBlockList.appendChild(newNode("h5")).appendChild(newText("Saved")); var numrecent = GM_getValue(RECENT_NUM); if (!numrecent) { GM_setValue(RECENT_NUM, 5); } var recentpos = GM_getValue(RECENT_POS); if (!recentpos) { recentpos = "bottom"; GM_setValue(RECENT_POS, recentpos); } if (recentpos == "top") { searchesBlock.appendChild(recentSearchesBlockList); searchesBlock.appendChild(searchesBlockList); } else { searchesBlock.appendChild(searchesBlockList); searchesBlock.appendChild(recentSearchesBlockList); } editLink = newNode("div"); editLink.appendChild(newText("Edit searches")); editLink.className = "lk cs editLink"; editLink.addEventListener("click", editPersistentSearches, false); searchesBlock.appendChild(editLink); if (getCookie(SEARCHES_COOKIE) != null) { restorePersistentSearches(); } else { for (var query in DEFAULT_SEARCHES) { addPersistentSearch(new PersistentSearch(query, DEFAULT_SEARCHES[query])); } } checkCurrentQuery(); insertSearchesBlock(); if (getCookie(SEARCHES_COLLAPSED_COOKIE) == "1") { togglePersistentSearches(); } checkSearchesBlockParent(); } function rearrangeSeachesBlock() { if (GM_getValue(RECENT_POS) == "top") { recentSearchesBlockList.parentNode.removeChild(recentSearchesBlockList); searchesBlock.insertBefore(recentSearchesBlockList, searchesBlockList); } else { searchesBlockList.parentNode.removeChild(searchesBlockList); searchesBlock.insertBefore(searchesBlockList, recentSearchesBlockList); } } function checkCurrentQuery() { var currentQuery = getCurrentQuery(); if (currentQuery) { var found = false; var recentSearch = new PersistentSearch(currentQuery, currentQuery, PersistentSearch.RECENT_TYPE); for (var i=0; i < searches.length; i++) { if (searches[i].equals(recentSearch)) { found = true; break; } } if (!found) { for (var i=0; i < recentSearches.length; i++) { if (recentSearches[i].equals(recentSearch)) { found = true; break; } } if (!found) { addRecentSearch(recentSearch); } } } } function getCurrentQuery() { var queryString = window.location.search; var split = queryString.split("&"); var params = {}; for (var i=0; i < split.length; i++) { var pair = split[i].split("="); params[pair[0]] = pair[1]; } if (params["search"] && params["search"] == "query") { return decodeURIComponent(params["q"]); } else { return null; } } function insertSearchesBlock() { var labelsBlock = getNode(GM_getValue(POSITION, "nb_2")); if (!labelsBlock) { labelsBlock = getNode("nb_0"); if (!labelsBlock) { return; } } getNode("nav").insertBefore(searchesBlock, labelsBlock); } // For some reason, when naving back to the Inbox after viewing a message, we seem // to get removed from the nav section, so we have to add ourselves back. This only // happens if we're a child of the "nav" div, and nowhere else (but that's the place // where we're supposed to go, so we have no choice) function checkSearchesBlockParent() { if (searchesBlock.parentNode != getNode("nav")) { insertSearchesBlock(); } window.setTimeout(checkSearchesBlockParent, 200); } function restorePersistentSearches() { var serializedSearches = getCookie(SEARCHES_COOKIE).split("|"); for (var i=0; i < serializedSearches.length; i++) { var search = PersistentSearch.prototype.fromString(serializedSearches[i]); if (search.type == PersistentSearch.RECENT_TYPE) { addRecentSearch(search); } else { addPersistentSearch(search); } } } function saveSearches() { var serializedSearches = new Array(); for (var i=0; i < searches.length; i++) { serializedSearches.push(searches[i].toString()); } for (var i=0; i < recentSearches.length; i++) { serializedSearches.push(recentSearches[i].toString()); } setCookie(SEARCHES_COOKIE, serializedSearches.join("|")); } function clearPersistentSearches() { for (var i=0; i < searches.length; i++) { var item = searches[i].getListItem(); if (item.parentNode) { item.parentNode.removeChild(item); } } searches = new Array(); } function addPersistentSearch(search) { searches.push(search); searchesBlockList.appendChild(search.getListItem()); searchesBlockList.style.display = "block"; saveSearches(); } function removeRecentSearch(search) { var removedSearchItem = search.getListItem(); if (removedSearchItem.parentNode) { removedSearchItem.parentNode.removeChild(removedSearchItem); } } function addRecentSearch(search) { while (recentSearches.length >= GM_getValue(RECENT_NUM)) { removeRecentSearch(recentSearches.shift()); } recentSearches.push(search); recentSearchesBlockList.appendChild(search.getListItem()); recentSearchesBlockList.style.display = "block"; saveSearches(); } function limitRecentSearch() { while (recentSearches.length > GM_getValue(RECENT_NUM)) { removeRecentSearch(recentSearches.shift()); } } function editPersistentSearches(event) { var container = getNode("co"); hiddenNodes = new Array(); for (var i = container.firstChild; i; i = i.nextSibling) { hiddenNodes.push(i); i.style.display = "none"; } searchesContainer = newNode("div"); searchesContainer.className = "searchesContainer"; searchesContainer.innerHTML += "Persistent Searches"; container.appendChild(searchesContainer); var innerContainer = newNode("div"); innerContainer.className = "innerContainer"; innerContainer.innerHTML += '
Use operators ' +
'to specify queries. today, yesterday and oneweekago ' +
'are also supported as values for the before: and after: ' +
'operators. ' +
'Prefix your search query with ++ ' +
'to make a search that will ADD your query to the current query. ' +
'Delete an item\'s query to remove it.