` command takes a message id or a username match to highlight
81 | that particular message or user's messages. Calling it without any arguments
82 | will list all active highlight rules.
83 |
84 | ### Clipboard
85 |
86 | The clipboard is a place to store messages and notes to yourself.
87 |
88 | * `/jot [match]` is used to take down messages or notes.
89 | * If `[match]` is a number, than the message with that id is taken
90 | down into the clipboard, else everything else after `/jot` is
91 | stored into the clipboard as a note.
92 | * To take transcript message to the clipboard simply use
93 | `/jot [id]|Description` format.
94 | * After jotting down things into the clipboard, an id will be shown.
95 | This id will be used to refer to the note when pasting or
96 | removing the message from the clipboard.
97 | * `/clips` will show a list of all messages and notes taken. Clips and also be managed from here.
98 | * `/paste [id]` if a message is pasted, that message will be **quoted**,
99 | else a new message with the note's content will be generated
100 | * `/rmclip [id]` will remove the clip with that id from the clipboard
101 |
102 | ### Keyboard Navigation
103 |
104 | * Ctrl + Up Arrow begins navigation
105 | * Ctrl + Down Arrow cancels navigation and jumps to message input
106 | * Up Arrow and Down Arrow navigate between messages
107 | * Page Up and Page Down navigate more quickly (5 messages at a time)
108 | * On a selected message:
109 | * Q quotes the selected message
110 | * E edits the selected message
111 | * R begins a reply to the selected message
112 | * D removes the selected message
113 | * S stars the selected message
114 | * H shows the history of the message
115 | * F puts the command to flag a message in the input box
116 | * J jumps to the replied-to message, if the selected message is
117 | an explicit reply
118 | * C jots the selected message
119 | * Right Arrow edits or begins a reply to the selected message,
120 | depending on if you own it
121 | * Left Arrow and P display the replied-to message, if the
122 | selected message is an explicit reply
123 |
124 | ### Other features
125 |
126 | The script will add the id of every message as well as its timestamp on
127 | the right of every message, to aid in using some of the commands. Rate
128 | limited messages can be retried with `ctrl` + `space`
129 |
130 | ## SEModifications
131 | Provides minor tweaks to StackExchange sites.
132 |
133 | [Stack Apps link](https://stackapps.com/questions/2138/se-modifications-username-autocomplete-in-comments-inline-revision-source-a)
134 |
135 | * Adds a timeline link to each question page
136 | * Turns comment timestamps into links to the comment, so that they can be linked to.
137 | This will only work if the comment is visible when the page is linked to - posts with more than
138 | 5 comments and 20 comments on Metas will only show their top voted comments
139 | (Unless you have this script).
140 | * Adds audit link to /reputation
141 | * Adds history link to each question and answer
142 |
143 | ### Autocomplete
144 |
145 | 
146 |
147 | This script also adds chat-like autocomplete for comments replies, in a way (relatively)
148 | consistent with [these criteria](http://meta.stackoverflow.com/questions/43019/how-do-comment-replies-work/43020#43020).
149 | Note that while editors can be notified, only the **last** editor's name will show up in the list.
150 |
151 | ## SEReputationAuditHelper
152 | This script adds certain useful information to the reputation audit page (`/reputation` on all SE 2.0 sites):
153 |
154 | * Autolinkifies the post ID associated with each reputation event
155 | * Adds description for each vote type instead of seeing vote ID
156 | * Adds a links to reputation audit pages for all other SE sites for faster navigation
157 |
158 | ## SEChatFaviconNotifier
159 | 
160 |
161 | Adds unread count and @-mention notifications directly to the favicon of
162 | the chatroom page. The circle shows the number of unread messages and
163 | turns green when someone @-mentions you.
164 |
165 | ## SECommentLinkHelper
166 | Automatically converts raw Stack Exchange question links in comments into their
167 | correctly-titled `[title](link)` syntax equivalents before posting.
168 |
169 | [Stack Apps link](https://stackapps.com/questions/2378/se-comment-link-helper)
170 |
--------------------------------------------------------------------------------
/SEModifications.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name SE Modifications
3 | // @description A collection of modifications for the Stack Exchange network of sites
4 | // @match *://stackoverflow.com/*
5 | // @match *://meta.stackoverflow.com/*
6 | // @match *://superuser.com/*
7 | // @match *://meta.superuser.com/*
8 | // @match *://serverfault.com/*
9 | // @match *://meta.serverfault.com/*
10 | // @match *://askubuntu.com/*
11 | // @match *://meta.askubuntu.com/*
12 | // @match *://*.stackexchange.com/*
13 | // @match *://answers.onstartups.com/*
14 | // @match *://meta.answers.onstartups.com/*
15 | // @match *://stackapps.com/*
16 | // @match *://mathoverflow.net/*
17 | // @exclude http://chat.stackexchange.com/*
18 | // @exclude http://chat.*.stackexchange.com/*
19 | // @exclude http://api.*.stackexchange.com/*
20 | // @exclude http://data.stackexchange.com/*
21 | // @exclude http://*/reputation
22 | // @author @rchern
23 | // ==/UserScript==
24 |
25 | function inject() {
26 | for (var i = 0; i < arguments.length; ++i) {
27 | if (typeof(arguments[i]) == 'function') {
28 | var script = document.createElement('script');
29 |
30 | script.type = 'text/javascript';
31 | script.textContent = '(' + arguments[i].toString() + ')(jQuery)';
32 |
33 | document.body.appendChild(script);
34 | }
35 | }
36 | }
37 |
38 | inject(function ($) {
39 | var profile = $('#hlinks-user a.profile-link'),
40 | My = {
41 | 'name': profile.text(),
42 | 'profile': profile.attr('href')
43 | };
44 |
45 | $(function () {
46 | // Initialize the general stuff here
47 | var locationBits = location.hostname.split('.');
48 |
49 | if(locationBits[0] !== 'discuss' && (locationBits[0] !== 'meta' || locationBits[1] === 'stackoverflow')) {
50 | if (window.profileLink) {
51 | var initialized = false;
52 |
53 | profileLink._show = profileLink.show;
54 | profileLink.show = function (u, v) {
55 | profileLink._show(u, v);
56 |
57 | if (!initialized && (initialized = true)) {
58 | $('audit').appendTo('.profile-links');
59 | }
60 | };
61 | }
62 | }
63 |
64 | initQuestions();
65 | initPostRevisions();
66 | //initSearchResults();
67 | });
68 |
69 | /*
70 | * Initializes functionality that only appears on the questions page:
71 | * - Question timeline linking
72 | * - Explicit post history linking
73 | * - Comment linking
74 | */
75 | function initQuestions() {
76 | if (location.pathname.search("^/questions/\\d+/") === -1) {
77 | return;
78 | }
79 |
80 | var needsHighlight = false,
81 | question = $("#question-header a")[0].href;
82 |
83 | if (!question) {
84 | return;
85 | }
86 |
87 | var post = question.replace("questions", "posts").replace(/\/[^\/]*$/, ""),
88 | timeline = post + "/timeline",
89 | revisions = post + "/revisions",
90 | questionPost = $('#question'),
91 | ownedByMe = $('.post-signature', questionPost).length == 2 && $('.post-signature.owner .user-details a:first').attr('href') === My.profile;
92 | $(".post-menu", questionPost).append(
93 | (ownedByMe ? "
" : "|") +
94 | "timeline"
95 | );
96 | $(".post-menu").each(function() {
97 | var self = $(this),
98 | postLink = question,
99 | id = self.find("a.short-link")[0].href.replace(/^.*\/a\//, "").replace(/\/\d+(?:#.*)?$/, "");;
100 |
101 | if (!revisions) {
102 | revisions = "/posts/" + id + "/revisions";
103 | postLink = postLink + '/' + id;
104 | }
105 |
106 | self.append("|history")
107 | .find('a').each(function () {
108 | var nodes = this.childNodes, i;
109 |
110 | for (i = 0; i < nodes.length; ++i) {
111 | if (nodes[i].nodeType == Node.TEXT_NODE) {
112 | nodes[i].nodeValue = nodes[i].nodeValue.replace(/ +/g, '\u00a0');
113 | }
114 | }
115 | });
116 |
117 | revisions = null;
118 | });
119 | }
120 |
121 | /*
122 | * Initializes functionality that only appears on the post revisions page:
123 | * - Inline revision source loading
124 | */
125 | function initPostRevisions() {
126 | if (location.pathname.search("^/posts/\\d+/revisions") === -1) {
127 | return;
128 | }
129 |
130 | $('.revision, .owner-revision').find('a[href$="/view-source"]').one('click', function () {
131 | var self = $(this),
132 | original = self.text();
133 |
134 | self.text('loading...');
135 |
136 | $.ajax({
137 | 'url': this.href,
138 | 'context': self.closest('tr').next().find('.post-text')[0],
139 | 'success': function (response) {
140 | var id = "inline-" + this.parentNode.id;
141 |
142 | self.removeAttr('target')[0].href = "#" + id;
143 |
144 | $('', {
145 | 'text': $(response).filter('pre').text(),
146 | 'css': {
147 | 'white-space': 'pre-wrap'
148 | }
149 | }).appendTo(this)[0].id = id;
150 | },
151 | 'complete': function() {
152 | self.text(original);
153 | }
154 | });
155 |
156 | return false;
157 | });
158 | }
159 |
160 | /*
161 | * Initializes functionality that only applies to the search results page
162 | * - Removing/untaggifying tags from search results
163 | */
164 | function initSearchResults() {
165 | var tagpage = false;
166 |
167 | if (location.pathname.indexOf('/search') !== 0 &&
168 | !(tagpage = (location.pathname.indexOf('/questions/tagged/') === 0))) {
169 | return;
170 | }
171 |
172 | function on() {
173 | $(this).animate({ 'opacity': 1 }, 750);
174 | }
175 |
176 | function off() {
177 | $(this).animate({ 'opacity': 0 }, 750);
178 | }
179 |
180 | var criteria,
181 | tags = $('#sidebar .tagged a.post-tag');
182 |
183 | tags.each(function () {
184 | var tag = $(this),
185 | wrapper = $('')
186 | .append(
187 | $('')
188 | .append(
189 | $('"')
190 | .click(function () {
191 | updateCriteria(tag.text(), '"', true);
192 | wrapper.fadeOutAndRemove();
193 | })
194 | .add('\u00D7')
195 | .css({
196 | 'font-family': 'Consolas, monospace',
197 | 'border-radius': '7px 7px',
198 | 'display': 'inline-block',
199 | 'width': '15px',
200 | 'height': '15px',
201 | 'margin-right': '3px',
202 | 'text-align': 'center',
203 | 'border': '1px solid #915857',
204 | 'cursor': 'pointer'
205 | })
206 | .last()
207 | .click(function () {
208 | updateCriteria(tag.text(), false, true);
209 | })
210 | .end()
211 | )
212 | .css({
213 | 'margin-left': '5px',
214 | 'color': '#6C0000',
215 | 'font-weight': 'bold',
216 | 'opacity': 0
217 | })
218 | .hover(on, off)
219 | )
220 | .insertBefore(this)
221 | .prepend(this);
222 |
223 | // Should just not add it in the first place, will fix later
224 | if (tagpage && tags.length === 1) {
225 | wrapper.find('.remove-tag').remove();
226 | }
227 | });
228 |
229 | function updateCriteria(existing, update, isTag) {
230 | update = !update ? '' : update + existing + update;
231 |
232 | if (isTag) {
233 | existing = '\\[' + existing + '\\]|' + existing;
234 | }
235 |
236 | var search = $('#search'), input = search.find('input[name="q"]');
237 |
238 | // Temporary until I expand on this feature
239 | input.val(
240 | input.val()
241 | .replace(new RegExp('(^| )(' + existing + ')( |$)'), '$1' + update + '$3')
242 | .replace(/^ +| +$/, '')
243 | );
244 | search.submit();
245 | }
246 | }
247 | });
248 |
--------------------------------------------------------------------------------
/SECommentLinkHelper.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name SE Comment Link Helper
3 | // @description A hook to transform raw links to properly titled links in comments
4 | // @version 1.24
5 | // @homepageURL https://stackapps.com/questions/2378/se-comment-link-helper
6 | // @namespace https://github.com/rchern/StackExchangeScripts
7 | // @match *://stackoverflow.com/*
8 | // @match *://meta.stackoverflow.com/*
9 | // @match *://es.stackoverflow.com/*
10 | // @match *://es.meta.stackoverflow.com/*
11 | // @match *://ja.stackoverflow.com/*
12 | // @match *://ja.meta.stackoverflow.com/*
13 | // @match *://pt.stackoverflow.com/*
14 | // @match *://pt.meta.stackoverflow.com/*
15 | // @match *://ru.stackoverflow.com/*
16 | // @match *://ru.meta.stackoverflow.com/*
17 | // @match *://mathoverflow.net/*
18 | // @match *://meta.mathoverflow.net/*
19 | // @match *://superuser.com/*
20 | // @match *://meta.superuser.com/*
21 | // @match *://serverfault.com/*
22 | // @match *://meta.serverfault.com/*
23 | // @match *://askubuntu.com/*
24 | // @match *://meta.askubuntu.com/*
25 | // @match *://stackapps.com/*
26 | // @match *://*.stackexchange.com/*
27 | // @match *://*.meta.stackexchange.com/*
28 | // @exclude *://chat.stackexchange.com/*
29 | // @exclude *://chat.*.stackexchange.com/*
30 | // @exclude *://api.*.stackexchange.com/*
31 | // @exclude *://data.stackexchange.com/*
32 | // @exclude *://*/reputation
33 | // @author @TimStone
34 | // ==/UserScript==
35 |
36 | function inject() {
37 | for (var i = 0; i < arguments.length; ++i) {
38 | if (typeof(arguments[i]) == 'function') {
39 | var script = document.createElement('script');
40 |
41 | script.type = 'text/javascript';
42 | script.textContent = '(' + arguments[i].toString() + ')(jQuery)';
43 |
44 | document.body.appendChild(script);
45 | }
46 | }
47 | }
48 |
49 | inject(function ($) {
50 | function HijackedTextarea(t) {
51 | var filters = { questions: '-ox0X.YDyJfh', answers: '!b6vl_mZrb8iVXs' },
52 | textarea = t.addClass('link-hijacked')[0],
53 | form = t.closest('form'),
54 | span = document.createElement('span'),
55 | link = new RegExp('(?:^|[^\\w\\\\])https?://([^\\s/]+)/(q(?:uestions)?|a)/([0-9]+)', 'ig'),
56 | lock = 0,
57 | submitComment = $._data(form[0], 'events').submit[0].handler,
58 | // regex tested with
59 | // $.getJSON('https://api.stackexchange.com/2.2/sites?pagesize=1000&filter=!6P.Ehvyb0IX7X')
60 | // .done(data => {
61 | // const domains = data.items.map(item => new URL(item.site_url).hostname),
62 | // missed = domains.filter(d => !validSites.test(d))
63 | // if (missed.length) { console.error('Domains missed', missed) }
64 | // else { console.log('Test passed') }
65 | // });
66 | validSites = new RegExp(
67 | '^(?:' + // one of: top-level names, with their meta sites
68 | '(?:' +
69 | '(?:meta\\.)?' +
70 | '(?:' + // one of: .com names
71 | '(?:stackoverflow|serverfault|askubuntu|superuser)\\.com' +
72 | '|' + // or: mathoverflow.net
73 | 'mathoverflow\\.net' +
74 | ')' +
75 | ')' +
76 | '|' + // or: stackapps, which has no meta.
77 | 'stackapps\\.com' +
78 | '|' + // or: the language-variants of stackoverflow.com
79 | '(?:es|ja|pt|ru)\\.(?:meta\\.)?stackoverflow\\.com' +
80 | '|' + // or: .stackexchange sites, with their meta sites.
81 | '[^.]+\\.(?:meta\\.)?stackexchange\\.com' +
82 | ')$',
83 | 'i'
84 | ),
85 | miniLink = /(^|\W)(\[([^\]]+)\]\((?:(?:https?|ftp):\/\/[^)\s]+?)(?:\s(?:"|")(?:[^"]+?)(?:"|"))?\))/g,
86 | miniCode = /(^|\W)(`(?:.+?)`)(?=\W|$)/g,
87 | results = [];
88 |
89 | $._data(form[0], 'events').submit[0].handler = handler;
90 |
91 | function handler() {
92 | if (lock)
93 | return;
94 |
95 | lock = -1;
96 |
97 | var url, questions = {}, answers = {},
98 | comment = textarea.value.replace(miniLink, "$1##").replace(miniCode, "$1##");
99 |
100 | while (url = link.exec(comment)) {
101 | var type = url[2] === 'a' ? answers : questions,
102 | domain = url[1];
103 |
104 | if (!type[domain])
105 | type[domain] = [];
106 |
107 | type[domain].push(url[3]);
108 | }
109 |
110 | if (Object.keys(questions).length || Object.keys(answers).length) {
111 | request(questions, 'questions', callback);
112 | request(answers, 'answers', callback);
113 | }
114 |
115 | if (lock < 0) {
116 | // either no question and answer links were detected, *or* none of the
117 | // links were for valid sites and so no AJAX requests were started.
118 | // In either case we need to trigger the comment submit at this point.
119 | submit();
120 | }
121 |
122 | link.lastIndex = 0;
123 |
124 | return false;
125 | }
126 |
127 | function callback(data, domain) {
128 | lock = lock - 1 === 0 ? -1 : lock - 1;
129 |
130 | if (!data.items || !data.items.length) {
131 | if (lock < 0) {
132 | submit();
133 | }
134 |
135 | return;
136 | }
137 |
138 | data.domain = domain;
139 |
140 | results.push(data);
141 |
142 | if (lock < 0) {
143 | submit();
144 | }
145 | }
146 |
147 | function submit() {
148 | var i, j, id, post, pattern, swaps = [],
149 | swapper = function (s, m1, m2) {
150 | swaps.push(m2);
151 |
152 | return m1 + "~%" + (swaps.length - 1) + "#";
153 | },
154 | comment = textarea.value;
155 |
156 | if (results.length) {
157 | for (i = 0; i < results.length; ++i) {
158 | comment = comment.replace(miniLink, swapper).replace(miniCode, swapper);
159 |
160 | for (j = 0; j < results[i].items.length; ++j) {
161 | post = results[i].items[j];
162 | id = post.question_id || post.answer_id;
163 | pattern = '(^|[^\\w\\\\])http(s?)://' + results[i].domain.replace('.', '\\.') + '/(q(?:uestions)?|a)/' + id + '(?:/[-\\w]*)?(/[0-9]+)?(?:\\?[a-z]+=1)?(#\\w+)?';
164 | comment = comment.replace(new RegExp(pattern, 'gi'), function (s, leading, https, type, trailing, anchor) {
165 | leading = leading || '';
166 | trailing = trailing || '';
167 | anchor = /^#comment(\d+)_/.exec(anchor || '');
168 |
169 | var url;
170 |
171 | if (anchor) {
172 | url = '/posts/comments/' + anchor[1];
173 | } else if (type === 'questions' && trailing) {
174 | url = '/a' + trailing;
175 | } else if (type === 'a') {
176 | url = '/a/' + id;
177 | } else {
178 | url = '/q/' + id;
179 | }
180 |
181 | return leading + '[' + escapeMarkdown(cleanWhitespace(toText(post.title))) + '](https://' + results[i].domain + url + ')';
182 | });
183 | }
184 | }
185 |
186 | textarea.value = comment.replace(/~%(\d+)#/g, function (s, m1) {
187 | return swaps[+m1];
188 | });
189 | $(textarea).trigger('keyup');
190 | }
191 |
192 | submitComment.call(form[0]);
193 |
194 | results = [];
195 | lock = 0;
196 | }
197 |
198 | function toText(html) {
199 | span.innerHTML = html;
200 |
201 | return span.textContent;
202 | }
203 |
204 | function cleanWhitespace(text) {
205 | // remove leading and trailing whitespace, collapse multiple spaces
206 | // into one everywhere else.
207 | return text.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
208 | }
209 |
210 | function escapeMarkdown(text) {
211 | return text.replace(/\[/g, '\\[')
212 | .replace(/\]/g, '\\]')
213 | .replace(/\*/g, '\\*')
214 | .replace(/_/g, '\\_')
215 | .replace(/`/g, '\\`');
216 | }
217 |
218 | function request(ids, type, callback) {
219 | Object.keys(ids).forEach(function (domain) {
220 | if (validSites.test(domain)) {
221 | lock = lock < 0 ? 1 : lock + 1;
222 |
223 | $.get(window.location.protocol + '//api.stackexchange.com/2.1/' + type + '/' + ids[domain].join(';') + '?site=' + domain + '&filter=' + filters[type] + '&key=p0r10MZ01l1H4So8wqT*qA((',
224 | function (data) {
225 | // Go home Firefox you are drunk
226 | if (typeof(data) === 'string') {
227 | data = JSON.parse(data);
228 | }
229 |
230 | callback(data, domain);
231 | }
232 | );
233 | }
234 | });
235 | }
236 | }
237 |
238 | $(document).on('focus', 'textarea[name="comment"]:not(.link-hijacked)', function () {
239 | new HijackedTextarea($(this));
240 | });
241 | });
242 |
--------------------------------------------------------------------------------
/SEChatFaviconNotification.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Stack Overflow Chat Favicon Notifier
3 | // @namespace yijiang
4 | // @description Watches the SO Chat for new messages, and changes the Favicon accordingly
5 | // @include http://chat.stackoverflow.com/rooms/*
6 | // @include http://chat.meta.stackoverflow.com/rooms/*
7 | // @include http://chat.stackexchange.com/rooms/*
8 | // @include http://chat.askubuntu.com/rooms/*
9 | // @exclude http://chat.stackoverflow.com/rooms/info/*
10 | // @exclude http://chat.meta.stackoverflow.com/rooms/info/*
11 | // @exclude http://chat.stackexchange.com/rooms/info*
12 | // @exclude http://chat.askubuntu.com/rooms/info/*
13 | // ==/UserScript==
14 |
15 | (function(){
16 | function getLink () {
17 | return document.querySelectorAll('head link[rel*=shortcut]')[0];
18 | }
19 |
20 | var title = document.title,
21 | iconURL = getLink().href,
22 | img = new Image(),
23 | canvas = document.createElement('canvas'),
24 | ctx = canvas.getContext('2d'),
25 | host = window.location.hostname,
26 | sites = {
27 | gaming: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAXklEQVQ4jWNgoBa4z8HxHx0TI0e5AdgkScFEG/D7yZP/v588GUADkA2iyACKXYBhwFUGBrb9DAwcJGp8R1Q6wInZ2b+TlCKR8FOykjQM3+PgeE3QAGyGkaSJqgYQAwBdcz9MXASc9AAAAABJRU5ErkJggg==',
28 | cooking: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVQ4jc2SwWsaQRTG50/In7DgLBXcoYalF09DL5ZiJBQPEXIoQQI9Vei1Bw9SlAqCiLK7B10oIaWpbTEgZAl7SEBLFHvRaoOYQrJCECXrQUIOXw/BJVuTS0/94F1m3m/ee/M+Qv5LqT7Br0g0sYiiRHlRolxhNKtKYltjIiqRMFRJbC/DTCztBjnq6STODg20Cjnsb21if2sT9XQS414XC1UiYbhghXnWd4Mc1/YV5iML9mnfSZ6PLPTzGcxHlnOmMRGk6BUEhdGsJlFTY3T6c0dHM76NTioB+7QPq1ZFM76NRizqevDX189QJfqFqEwc1tNJWCffMe510UklYHAZjVgUjVgU/XwGl0emA1q1KgBAD8hQfYKfaEx0Lu9WuKtBWQEA3MxsdFIJjHvd2/YJIUSTqHnw+hVmF+f4/WnHVW0BmSEOAOjnM7BqVewGOVSf5yUhhJCsIKyoTCxpjE5P3r+DGeKuTgZlBccba5i0mzC4jEFZwd7zpyhKlC/tXmN0evDiGcwQx+WRCatWxfHGGgwu48fbN878rUIOikQTSx5QmGf9W+AxDC6jk0pgUFYwaTdxM7NdY50dGrcb+FtZQVj5+MQHg8uYtJv3fui1fYVKJAyF0ey9Nv6w6h0aXEY/n3GBs4tztAo56AH5YZgQQjQfjev+R2191etYWA/IUJk4VJlYKnoF4UH4X/QHlvTYdeSZTVAAAAAASUVORK5CYII=',
29 | webapps: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADIElEQVQ4jc1Tf0jTCRx9tX5B0CEUdVdcctcPgovyrAjKSqikFZOkEWdDrAxu+KMyM/JuZYZrXWszdf5Ylj+WmQ2bVlp2l9qmo5xzS115itXcmtbKH5tu+y63T/9MIYj+7sH768N7vA+8B3xXUOtM+24/aNZkKe51i4vvvrxxr6khq6h8C4BZAKZ9W6zvisirqOvMyFe+sQ+NfBq0D3lTLpW9EhYo27nR0Rui+fyg23XqBVwul/VVs9KaRo3W2O1gGK/fwzBUpnpIo84xf1x6Xm+q+LrxqOha+0FBTkd2WU1VWFjEjwBmTIn/1batzpQrn/fb7IzX6yWNzkjJwlzS6Iw07nL7Tb1mV89rq9syYGcEORXmv0WyLAALALBQ36L7raS6oT5RWNQ7Nu7yCfMVlCzMneJ5WSkxDEMMw5DL5aLHWoNzb8L5ToFEruTy+CtxuaTmyX21fsgxNu5r0XdQikhGow4nnRDm0qjDSSkXZNT0rJ0+Dg+T7d176jNb6MGTZ54UUYH9lChPg5i07B6Ph/Ezgb/zy1Xk9njoqaGLhkZGKLtUSTJFFfWZLWTq6SN95wvS6NpJVd/k2xj15xtwEkXdkxHVrUY6nplD7+wfyDowSK8tVopPl1Bl7X+ke95F6lY9PVJrqfpRIylUtf41ETFvwYkXGlo7/ncNvreT2WqjkyIZJaRL6S+JnPhnxJSYIaUGbSvVNTbTnYePqby6jooqVXT8nNgdFhXXj4sFFYXhvNRuabFyuFln8LW0GUh+q5oycor9FwvLfIo7tRPyiqoJ6bWbn85kFXrjBSJ3ZFyycz2bNxAVHavCitDQ+Qfjk/lJAnFbKCeuP116dexYhsQRwo61rt8daw3ZxXv7+64DtnVsnm3L3kMWTkzCq0OJqW2bt+64AmA/ALAABC0ODl6blJZZeSDhtOnIibOmPfv+KA3fyZZsZ3P+2RS+I3Ptug1pwb8uT/ppydLDYLEiAWwGsGyyS9MBzAawCEAogO0AtgEIAbAKwEoAKwD8AuBnAAsBzAts5AuwAMwFEATgBwBzAMwM1HZG4D49sIVpAPAZrcDAsT9JO/sAAAAASUVORK5CYII=',
30 | programmers: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABcUlEQVQ4ja3PsYoaYRSG4VPogJ2d6AXI3oBgOc0MgzCChUiCyEyzLLvl3sCylZ07zcKAoGA6y8Q6hB8kF2FhiEbCVDY6M+L/ptphA6sSNh+c7pyH74j8z6xvboiCgHchvx8eiB4f+X53x5erK9T19WUwen4+ufTZsvhkmueR6OmJH67Lzw8f+XV/T3R7yzfHyY6++v773vqnWJaF7/t4nkev16PT6dBut6nX61Sr1fNNWq0Wg8GA19lutywWC5RSNJtNGo3G24ht2/T7fcIw5Hg8kqYpaZpyOBwyYDQaYdv2aUBEZDgcorUmSZJsXoAgCE4DpmnieR5hGKK1Jo7jDEjTFKUU4/H4NCAiUqvVmE6nAH81SJIEpRSTyeQ8ICLiui77/T47jOM4azCbzS4DjuOw2+0A0FqjtQZAKYVS6jLQ7XaZz+csl0vW6zWr1YrNZnO+gWEYGIZBPp+nUCiQy+UoFouUy2VKpRKVSuXNERH5A+OVSIxr3NrHAAAAAElFTkSuQmCC',
31 | math: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACI0lEQVQ4jX2Tv2/UMBzFvy2VECe18lXt2UnsOCRm759wf8ItqLmNibkSK5RMHbrcxEAz3IBYqSokhJAgMIF0l1BUCRZKEJWgonXzJzyWJPT3kzz5fT+y37OJrtCxigZWRaVVUXnsRoOrfJfqreuPD6WBrdehF+Gb8MfXDpUsYFaa5K8XrKSOyp45CnsiwHf3NnLuY8p1diRN/8gzo5IF7MzwkTSJlVF1ou7ASoMdV1epq5A6CoXQ+LAs8csL0eyfKFP9kWHSAh4Q4VNPwSqD3zLCO66Quj5SVyHnPtLOAjZn5/DVCWClwU8vRC58tIAhEWIiPLnZQc595NzHq2VvtOWoMud+uUEz2ZAIQyK86PaQC41C6LOAIRHWiVrAZEn2R4yxjDEWEyWNJ+0stJ4WUBuqBjDtqXHGGDu1H8RE480bc/gsNHa5rnLur7WAfTdMfnghXi4KHHghTqTZPg0YELFjFY2bWi+EeJ8I75c9WGlw4EXYExoTIftNXTFR/zERdoWGVQalG+LL+QxiImzMzKIQGjn3se+FIyujynpR+XrRGcW1Z6fLL2bQANaJUNQJH3ghrDKwyuBQRtjpcgyJsHVr/iJglWhtSFQ9qkN8Pt+t1oiwzXrtS3yz6FRx3ULBdTUR/v8MmqAeEiUfmQhioqw5VV5facpVdpdo5WlnITkd8KVaJRrHde+F0MiFxrSnrv9M5xUT3YuJyoLrcrIk+1f5/gHh6Co+e0undAAAAABJRU5ErkJggg==',
32 | webmasters: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADF0lEQVQ4jYWTXUiTcRTG/3URqJGQFUIaZgVRN4FWVF5UBNWFCrpQS6yMog+XkDXLPiFaNa1ZSbmKKDCycGQL3aau5XRzvdv71+Wm5sfr2mzONzffVyuNiqeLLFOkfvDAgfOcm3OeQ8g4Fy5g5q8KMwghhIrIsgWxwRbEBioi6+/ehHccixchDI9IVkCc3YdQKiCeFSCLvlm9JLKobjUrQEYFxNt9CGUFxDE8Ii1ehBBCCPGnpuZ2PSw/x4rIpAIkTYM/jhZSX/WpBm54hcqEVQ8aceJ1zw+Zqbv7hXvkLBUgMQ9+37Nf474ZedEqIf6MDHAlKrhMNom0umvO5sdvnMnP7EhVU1xmPCi096HI0Y+Tpm4kPDSPrVQZE3K1njP7XvQi5ooNpD8trc0tv0pbBkYzI4p0xQuVOsQodTjdyEHB9qHq4xhqhr7hZAOH+YoqRCi0fGlL8GByWYd7kfzNK0IIIUYes2v7v5zY9oT5vv5BA06ZepBv5iBr9qK0/xOe8qOo9IxgzX0T4h+ZkaJ2KI08Zk9aZIamVXrW+h6KZh8ytS4o34uTdJ1+wCFdG7Jr2hF9w6AnU9n90nnxHOPFfmMnlt02QNbiw6WuwIQYD+Lu1SPf3ItNj5n2ScNMANGP3gXuJj93IOnlW4TLNVhQWI0UrQunnTzOOHmU933GTkMHdmpdKGjkWpkAov9kgIpIr/WNbY0o1At7Tb1YfseIcLkG4XINtqibkcf6cK9HxLoyK9aUMThS13mFiki3eBFC9H6EURFSu4i1MSWG/CxdB7LrOcxTVCG2xABJTSdyLB5caxtEdu07RCnrnEZ+NIGKkOr9CCPjsd1Oh5HECjggNXTqE9UObFW/xcYKB7Zp2nHc4kHuaw67NK3+SvdwAR1GEhWxfSLKIuZaA4hiRSy1+xCarXMeKWjkOuYV6b/Ov1aDY8Yef1IFW3xQ71jAilhqDSDKImLulDv8epTfUBFZTUNDG0sHS4vVwfpb//JOi01AbGWw6a4qoML5j4eR409c/N+hqeT4ExfnDaS68gZSDDtcK2dN5/kJwrUeiRdJOWQAAAAASUVORK5CYII=',
33 | askubuntu: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABXElEQVQ4jZ2Tv4rCQBDG5w1sfAg761Q2NmG3FyuxFAKWeQCLq2yFVNfYp0l3xRWBtLKlmEUliErgslxCOCTwXaFZ/8RDz4WBTXbmN7PfzhDdrIVJrZDRu+SUSE44WRJychcmtW799ZJtqoWc3IuguxYy+pRtqlWCJaPZo2BtjGZXkGcy36tE3/mZgCzwsLHNW0iPToI9BOTCrwI4uSQZLSUnRJaBIlUoUoX9eICNbeKwWwEAtqOuBih3gv14oCFUbpadOpadOmLHxmG3wtf0Dbnw9VkufPyEQv+rAGLHRpEq5MIHAESWgVz4KFKlKwCA2LErgKQUSbkTrPsNAMC638C630AWePj+mCIXvk5yqYUWscyYCx9Z4CF2bP0dWQaUO0FkGdiOusgC7yzis8/4Ry/0jo3E6PMVwJxT87VWPtvwepj+WYlu58o4H2cjuXLm5IaMenNOTclpWPr8AvZxHtvnNf8+AAAAAElFTkSuQmCC',
34 | photo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACRElEQVQ4jaXDW0tTAQDA8fM9woqEqMAo6EGEHgrrIcUbuZvNTJfOSzl1KmJeZom6s6l5zaUV4iW3SZm99GBSJEihSEQWZMtkOtfOPHA8NMbYv+/QfvATSJAQj8cJ1VhQdVrU/HwO9XpUnZ4Do5avdzKZEVMZHj6L8+l5WlavUSXfpOKwmJZQPfF4HCEWi/G7tJqtnEI2Mwv5mWEgmJHHD8Ml5ptPMdR7lDFHEiNPkrGtpVF+mMOtmIZapZxYLIYQjUZ5Vj3MkN5Of34PI3mdzGjqma3Ipb/7DAN9x5gePMKiO4nnG6fpCl+k4m82VrmcaDSKEIlEsNdOcd84Sqd+gC7dQzpviLRV1tHSno3TcYH5iRMsLx1n+XsyM/sptCqXaZTLiEQiCKqqMmiZQCzoo1cr0qdxIBZ001bWiqXJQr29mEH3VRbWzrH06yRzeyl0yOk0HZhRVRVBURT6rZPYjQM4tA56NSKi3kGL2U6RrYcslxPj63Ycnwx4N9KZ+aDB+bYWcfUBiqIgyLLMvdF1LLb3WGsWaKh0Y61ZwGxbItf1jlTvImkvvJROTzA6OIS7fZy5BjcvOxaRZRlBkiTKvSG0kwfoxvcxjO2iG9/n+mSYrFmJKx4fOY++0Fz3mamSj7wxrbBStM561TckSUIIBoM0vtrD5AlT7Alz2xvC5JEweSRKvBKm+T80DQd5fDfAcrGfTbMfnznAVsMuwWAQIRAIIMvyfw0EAgh+v59ECjs7OyRS2N7eJpGCz+cjkYLL5SKR/wBu9SXq7QC25AAAAABJRU5ErkJggg==',
35 | superuser: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABM0lEQVQ4jZ3TMUsDQRCG4deAkECI4K+wsVBRFKxsLGy1E+wsLARBSyFdsJE0NrFQMEW8na1stBAb/QE2GguVVOLd7qZUMN5aXBSREHM3MNWwD98yu5BUFfBADHjyxRoA9dY12r2jzCWBWaBPfQOnwBqF0iwAtftFlN1FXIh2H4hZ/Q/Y6TkNXucQ10GZkLotpQcAlH1Cu5jAzGQEoiZiYwI3nwbwgGdqfZggaqKdzwLErGyNEkR33QRLWa6Q4/hxH+084m5pmGnK5VwaAI6u8ihzgHYeZTtpE0AjmkTsC9rFiL1JD4g9R2yMMnt/4w8KJFuQcKLXeDAg2cJ4NkDbh+476AucAZvkR5Kfd9hcRpkNVLSN2DeU/UTMWC+gArR/ulCsAnDSukC7drefEVcBhn4f/AKBs7SVr7eXywAAAABJRU5ErkJggg==',
36 | serverfault: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAkUlEQVQ4jWNgoBRcvHTlPzGYgYGB4aWG7v83str/38hq/3+tbvj/tYrif6IMuHb1LtwAZPxWUYw4A5Bd8ExHH8UQioOAYcXKtf+JwQwMDAwzFJX/o+OBNWC+rDQVwiAqKuY/MZiBgYFBX1z8PzoeBAZQA/yHYQ5Obqw0FDNwcHL/R8cM6JrxYPwGYHMJGhurAQAacYAPCrFaGAAAAABJRU5ErkJggg==',
37 | english: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAB4UlEQVR4nO3aMZHjMBQGYEMIhIWgJpYsN4FgCIEQCGHwlkEgBEIgBEIgBMJeYTuTzLm9U7HfN6NGav7qzdOTug4AgH8s+mGKXC9R6jlS2rXOA7Ap+mH6LuPPuiLXy8d5qcd5f3xGqcdGMQG6Lko9R66PSGkXuV7+Klj9MM3Fqhwij7dWOQG6yMNpLVKR0u61+mGKUs9RymEuVvWqYAFNrVe+7zL+rDOsyPX+fk18nffD1Dov8Est17xnlPIVeTjFvqal47pHKYdlGH+fB/Llq3Ve4BfbuuYtBev66rjmonVtlRGg67quizzeItf7x15Ku8jjbSlYx2XobnYFtPXeSW2e72uKPD4jj/GfowF8ilIOb/+vHpHHmF8G6/nVZS0zrtZZAdZf7o/NV8FcH7GvqXVGgA/zi+Fwmrur+cWwdSYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC2/QE1fKVGw+uhXAAAAABJRU5ErkJggg==',
38 | apple: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAADe0lEQVR4nO3WO2iddRgG8D8WHboUHEIXUXTJoCgYvIAVioKgdRAURCiaUkXbqRWkCIEODtqhdqnWSy/BFISAaRI0TSVUKJg0qdY0pxeaNrc25/vynVxMck4yvg6ihVLURc6Q3w9e/nzP9EwPX0q33NXX1/dET0/Pc/903d3dzzQ3N9+XUro7AdTBupTSAzMzMyfm5+fP/NsVRfH9li1bHk0p3VPv4sDa05BlWdfKykr81yuXyz+klO6vd3Fg7XmkUqmcrdVqUavV4mbxe5TGi/jr+07Z/Pz8ZErp4XoXB9aex4uiGFpeXo6R8ZnY03YuPum4EF+cuhzVavWO2dzc3FRK6bF6FwfWnqaiKIaq1Wr0/joZ354ZjcXqauw+2h8rKytx8peJ6Dw7Fvn8cuw+2h+1Ws1gAXXTlGXZ0NLSUgxfz2PH5z/Fgc7zf78tbf3R0jYQLW0DcaDzvD8soK6asiwbWlxcjGq1GjeLhRi+nsdb+0/G8PU8jvReiCO9F+K3a3msrq7G0tJSVCoVgwXURdPFixe7siyLoiji6sR0DF6ajNc++i6GrtyIg52DcbBzMPpL4zE7Oxt5nsfExMSVZLCAOmgqlUpd5XI5Tg9djpc+PB57vjz15/vVqdi270Rs23cimvd1xAeHTkaWZTE+Pm6wgLpoGhkZ6SqXy/F1d398fPx0zMwuxOZdh6NWq0Vrz7k42NEf127ksXnX4cjzPMbGxgwWUBdPDwwMHJqeno6+wUvx1I7P4pWWb2LHpx2xsLAQp89duZXt74g8z6NUKvWmlJ6sd3Fg7Xk2pdQ4PDzcXi6XJ0pXJ+d+HLi0VKlUylNTU9duz0ZHR39ubW19M6X0Yr2LA2vPvSmldzds2PD81q1bXz527NjO9vb29/fu3fvGpk2bnrs9a2xsfCGltD2l9GCdewNrVENK6dV169a93dDQ8N7GjRt3rl+//p2U0vY7ZK+nlB6qb10AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgP/VH5yFPolq4uGdAAAAAElFTkSuQmCC',
39 | unix: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAACnUlEQVR4nO3bv2sWdwDH8e+f4J/wLJE7pxsaeJ5Hh6dd6iBWSCkm3jUP0kDrEAVNRUhKhkKhlNRFRBDT5cktQh2dPHOLdIqbEFIu+A9EcHD8dsnZ/Kh066X4esF3ueHhM7057rkLST8fJcN89eAJIYRD1/rFOAB0bT9YL9NhEdNhEdtgpYP8bjosYjLI3wgWcGIkw3y1Ddah64PieRswgBPhaLAm1XZW1k11Pl+JC8sP323Uf97oeiNACOF4sDbq5veybuKnXy7tfXXt5xdl3cR/+w2A/8TRYJV1U5V1E8/nK3H+5r2m3GyarjcChBBCOD3IL7XBmpqeyyb1zrism5h9thB/+u1Z3L/jqtozqbazrjcDH7F0kO+mwyKeOfv169nFta3RzFL85PPv4rc/PIpffPPj09nFta2rdx7sXb394FzXW4GP3NT0XJYOiydnL15/O5pZiuPv7/9x65fHu6OZpdjefV2YX32R9C/3ut4KEEIIoaybWNZN3Nhsfp1U29nC8sN3bbDWq1e9rvcBvFduNs1+sPbWq1e92cW1rTZYG5vNludXwIkxqbazg9E6GKz3D93rnXHXOwGO+dBb8FPTc1kvu3Sqq10Ax/xTsJL+5V46LGLSz0cdTgP4236YnrTBSgf53XRw5Ub7sbR/DIETI+kX42RQPP/Q6XofAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8H/0F7olCY2MH7ueAAAAAElFTkSuQmCC'
40 | };
41 |
42 | canvas.width = 16;
43 | canvas.height = 16;
44 |
45 | ctx.font = "bold 8px Ubuntu, 'Segoe UI', Tahoma, Arial, 'sans serif'";
46 | ctx.textAlign = 'center';
47 |
48 | // Find the correct favicon to use
49 | if(host.indexOf('chat.stackoverflow') === 0) {
50 | img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABnElEQVQ4jWNgwAMaGBiYHqdqT36SpvP/carOgdueKuz41OM2JE177ZM0nf+PUrUXkmwAAwMDw91QJf7HKTqnn6Tp/H+UplNNUMOrqaw5r6ew1D2fxCAKE3uYpKv0OEX3KdQlETg1P+5j4Hw9he3q66ls/19NZX35agpLzcsJDOIMDAwMj5O1zR6n6P54nKb942GqpgtWAxoaGJheTWVQeTeNqe31VJbPUINev5rB0ny/gUHgSbaqz5Mk/T+Pk3UbCXrlQT+D5NupTE3vpjG9fTOV9f+racxvXvZzND0tk/JrYGBgwtCwqtCSc3WFQ/GqEqfAZYWO2vMTFDgYGBgYrjaI8jybyFn0bhrTo3fTmf6/mc4gjdXGVcW2WmsrHP/D8OpypzcrS10OrC53bN1QaRVypFkj6FaPSCROJ88sd+FfV2YdtKHCrmV9he3ODeW29zZU2P1fX2H/f2254//VZU7/VxY72WFozM3J+Z+bk/Mfq6HFviJzy31tFld4payscJ44s8BdEkM9sgCMjQtjtZCqBhADcBpAKqaKAQB1iiloT36niAAAAABJRU5ErkJggg==';
51 | } else if(host.indexOf('chat.meta.stackoverflow') === 0) {
52 | img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABWklEQVQ4ja2QvUpDQRCFLykkhYhFkGApVj6AlVhZikiw8AEsbxpDGiPu2Znd2CQEkXAJ3DuzeQAfwNLa2tpCLERSWaWQtUlAMNcfcODAsMz55uwkyTcFoOKcu/beR+fcHYCl7+ZLId77G+99ZObxnwEzyIpz7n6W5OxHg4ikInIxGo1qnyAbzPzsvY9EdFxqVtWqiDyoalTVFxE5z7JsLUmSxDm3zcxT59yUmffK4lbyPN9U1UsReVPVKCKvIQQeDAar3W53n4jeicj++JXhcFgPIbCqTmaJJnmec7/fPwBQWbS9CuAUwCGALQDVGWi5KIrT8Xj8FEKIRVGsl8XfstbGuYwxE2PMnTHGM/NRr9drZFlWfsBms7kCoAHAE9GttfaRiCIRRWttBBAB7H4xpmka0zSNi6CtVqvW6XR2AJwAuGq32/Uv858f5n2ZFi78V8BvqhTwV/0L4AMhxDwD8Y9esAAAAABJRU5ErkJggg==';
53 | } else {
54 | var smallIconUrl = document.querySelectorAll('#sidebar-menu .small-site-logo')[0].src,
55 | found = false;
56 |
57 | for(var i in sites){
58 | if(smallIconUrl.indexOf(i) !== -1) {
59 | img.src = sites[i];
60 | found = true;
61 | break;
62 | }
63 | }
64 |
65 | if(!found) {
66 | img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA5ElEQVQ4jWNggIKG/ftZFt19l7Ho9rtKvPjuu4yG/ftZGNDBkrvvtqx88OE/MXjqjTerMQxYee/DT2I0T7/55n/LxRffMAygGBQf+PIfHw7d/PG/2dL3cEySAeiasRpAMUiceec/OnZovvpfNvcsVkzQAHyasRpAMUho3vwkuW3bf6ecZf8VAqfhxfKBUz+LOkzlQTEgqXnLa2I0QwyY9kfGvU8IxQD7rEW+CgHTTsoHTDsDx4FTrysETP2BZsAtuYApsUR7DU1zBslhoxA47b9CwNSP8v5TsknWDDNAzneKNi55AC2gU1dDOLAVAAAAAElFTkSuQmCC';
67 | }
68 | }
69 |
70 | // Start title watching process
71 | setInterval(function(){
72 | if(title !== document.title) {
73 | title = document.title;
74 | var times, mention = false, ctitle = title, url;
75 |
76 | if(title.indexOf('(') === 0){
77 | ctitle = title.substring(1, title.indexOf(')'));
78 | if(ctitle.indexOf('*') > -1){
79 | mention = true;
80 |
81 | if(ctitle.length === 1){
82 | times = 0;
83 | } else {
84 | times = parseInt(ctitle, 10);
85 | }
86 | } else {
87 | times = parseInt(ctitle, 10);
88 | }
89 | } else {
90 | times = 0;
91 | }
92 |
93 | if(times > 0 || mention) {
94 | // Draw ma circlez!
95 | ctx.clearRect(0, 0, 16, 16);
96 | ctx.fillStyle = mention ? "rgba(63, 175, 7, 0.8)" : "rgba(200, 0, 30, 0.6)";
97 |
98 | ctx.drawImage(img, 0, 0);
99 |
100 | ctx.beginPath();
101 | ctx.arc(11, 11, 5, 0, Math.PI*2, true);
102 | ctx.fill();
103 |
104 | ctx.fillStyle = "#fff";
105 |
106 | if(times < 10){
107 | ctx.fillText(times, 11, 13);
108 | } else if(times < 100){
109 | ctx.fillText(~~(times/10), 9, 13);
110 | ctx.fillText((times%10), 13, 13);
111 | } else {
112 | ctx.fillText('+', 11, 13);
113 | }
114 |
115 | url = canvas.toDataURL("image/png");
116 | } else {
117 | url = iconURL;
118 | }
119 |
120 | var linkEle = getLink(),
121 | newLink = document.createElement('link'),
122 | head = document.getElementsByTagName('head')[0];
123 |
124 | newLink.rel = 'shortcut icon';
125 | newLink.type = 'image/png';
126 | newLink.href = url;
127 |
128 | head.removeChild(linkEle);
129 | head.appendChild(newLink);
130 | }
131 | }, 500);
132 | })();
133 |
134 |
--------------------------------------------------------------------------------
/SEKeyboardShortcuts.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name SE Keyboard Navigation
3 | // @description VIM like navigation of Stack Exchange sites
4 | // @include http://stackoverflow.com/*
5 | // @include http://meta.stackoverflow.com/*
6 | // @include http://superuser.com/*
7 | // @include http://meta.superuser.com/*
8 | // @include http://serverfault.com/*
9 | // @include http://meta.serverfault.com/*
10 | // @include http://askubuntu.com/*
11 | // @include http://meta.askubuntu.com/*
12 | // @include http://answers.onstartups.com/*
13 | // @include http://meta.answers.onstartups.com/*
14 | // @include http://nothingtoinstall.com/*
15 | // @include http://meta.nothingtoinstall.com/*
16 | // @include http://seasonedadvice.com/*
17 | // @include http://meta.seasonedadvice.com/*
18 | // @include http://stackapps.com/*
19 | // @include http://*.stackexchange.com/*
20 | // @exclude http://chat.stackexchange.com/*
21 | // @exclude http://chat.*.stackexchange.com/*
22 | // @exclude http://api.*.stackexchange.com/*
23 | // @exclude http://odata.stackexchange.com/*
24 | // @exclude http://area51.stackexchange.com/*
25 | // @exclude http://*/reputation
26 | // @author @rchern
27 | // ==/UserScript==
28 |
29 | function with_jquery(callback) {
30 | var script = document.createElement("script");
31 | script.type = "text/javascript";
32 | script.textContent = "(" + callback.toString() + ")(jQuery)";
33 | document.body.appendChild(script);
34 | }
35 |
36 | with_jquery(function ($) {
37 |
38 | /**
39 | * jQuery.ScrollTo - Easy element scrolling using jQuery.
40 | * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
41 | * Dual licensed under MIT and GPL.
42 | * Date: 5/25/2009
43 | * @author Ariel Flesler
44 | * @version 1.4.2
45 | *
46 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html
47 | */
48 | (function (d) { var k = d.scrollTo = function (a, i, e) { d(window).scrollTo(a, i, e) }; k.defaults = { axis: 'xy', duration: parseFloat(d.fn.jquery) >= 1.3 ? 0 : 1 }; k.window = function (a) { return d(window)._scrollable() }; d.fn._scrollable = function () { return this.map(function () { var a = this, i = !a.nodeName || d.inArray(a.nodeName.toLowerCase(), ['iframe', '#document', 'html', 'body']) != -1; if (!i) return a; var e = (a.contentWindow || a).document || a.ownerDocument || a; return d.browser.safari || e.compatMode == 'BackCompat' ? e.body : e.documentElement }) }; d.fn.scrollTo = function (n, j, b) { if (typeof j == 'object') { b = j; j = 0 } if (typeof b == 'function') b = { onAfter: b }; if (n == 'max') n = 9e9; b = d.extend({}, k.defaults, b); j = j || b.speed || b.duration; b.queue = b.queue && b.axis.length > 1; if (b.queue) j /= 2; b.offset = p(b.offset); b.over = p(b.over); return this._scrollable().each(function () { var q = this, r = d(q), f = n, s, g = {}, u = r.is('html,body'); switch (typeof f) { case 'number': case 'string': if (/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)) { f = p(f); break } f = d(f, this); case 'object': if (f.is || f.style) s = (f = d(f)).offset() } d.each(b.axis.split(''), function (a, i) { var e = i == 'x' ? 'Left' : 'Top', h = e.toLowerCase(), c = 'scroll' + e, l = q[c], m = k.max(q, i); if (s) { g[c] = s[h] + (u ? 0 : l - r.offset()[h]); if (b.margin) { g[c] -= parseInt(f.css('margin' + e)) || 0; g[c] -= parseInt(f.css('border' + e + 'Width')) || 0 } g[c] += b.offset[h] || 0; if (b.over[h]) g[c] += f[i == 'x' ? 'width' : 'height']() * b.over[h] } else { var o = f[h]; g[c] = o.slice && o.slice(-1) == '%' ? parseFloat(o) / 100 * m : o } if (/^\d+$/.test(g[c])) g[c] = g[c] <= 0 ? 0 : Math.min(g[c], m); if (!a && b.queue) { if (l != g[c]) t(b.onAfterFirst); delete g[c] } }); t(b.onAfter); function t(a) { r.animate(g, j, b.easing, a && function () { a.call(this, n, b) }) } }).end() }; k.max = function (a, i) { var e = i == 'x' ? 'Width' : 'Height', h = 'scroll' + e; if (!d(a).is('html,body')) return a[h] - d(a)[e.toLowerCase()](); var c = 'client' + e, l = a.ownerDocument.documentElement, m = a.ownerDocument.body; return Math.max(l[h], m[h]) - Math.min(l[c], m[c]) }; function p(a) { return typeof a == 'object' ? a : { top: a, left: a} } })(jQuery);
49 |
50 | /*
51 | * jqModal - Minimalist Modaling with jQuery
52 | * (http://dev.iceburg.net/jquery/jqModal/)
53 | *
54 | * Copyright (c) 2007,2008 Brice Burgess
55 | * Dual licensed under the MIT and GPL licenses:
56 | * http://www.opensource.org/licenses/mit-license.php
57 | * http://www.gnu.org/licenses/gpl.html
58 | *
59 | * $Version: 03/01/2009 +r14
60 | */
61 | (function ($) { $.fn.jqm = function (o) { var p = { overlay: 50, overlayClass: 'jqmOverlay', closeClass: 'jqmClose', trigger: '.jqModal', ajax: F, ajaxText: '', target: F, modal: F, toTop: F, onShow: F, onHide: F, onLoad: F }; return this.each(function () { if (this._jqm) return H[this._jqm].c = $.extend({}, H[this._jqm].c, o); s++; this._jqm = s; H[s] = { c: $.extend(p, $.jqm.params, o), a: F, w: $(this).addClass('jqmID' + s), s: s }; if (p.trigger) $(this).jqmAddTrigger(p.trigger); }); }; $.fn.jqmAddClose = function (e) { return hs(this, e, 'jqmHide'); }; $.fn.jqmAddTrigger = function (e) { return hs(this, e, 'jqmShow'); }; $.fn.jqmShow = function (t) { return this.each(function () { t = t || window.event; $.jqm.open(this._jqm, t); }); }; $.fn.jqmHide = function (t) { return this.each(function () { t = t || window.event; $.jqm.close(this._jqm, t) }); }; $.jqm = { hash: {}, open: function (s, t) { var h = H[s], c = h.c, cc = '.' + c.closeClass, z = (parseInt(h.w.css('z-index'))), z = (z > 0) ? z : 3000, o = $('').css({ height: '100%', width: '100%', position: 'fixed', left: 0, top: 0, 'z-index': z - 1, opacity: c.overlay / 100 }); if (h.a) return F; h.t = t; h.a = true; h.w.css('z-index', z); if (c.modal) { if (!A[0]) L('bind'); A.push(s); } else if (c.overlay > 0) h.w.jqmAddClose(o); else o = F; h.o = (o) ? o.addClass(c.overlayClass).prependTo('body') : F; if (ie6) { $('html,body').css({ height: '100%', width: '100%' }); if (o) { o = o.css({ position: 'absolute' })[0]; for (var y in { Top: 1, Left: 1 }) o.style.setExpression(y.toLowerCase(), "(_=(document.documentElement.scroll" + y + " || document.body.scroll" + y + "))+'px'"); } } if (c.ajax) { var r = c.target || h.w, u = c.ajax, r = (typeof r == 'string') ? $(r, h.w) : $(r), u = (u.substr(0, 1) == '@') ? $(t).attr(u.substring(1)) : u; r.html(c.ajaxText).load(u, function () { if (c.onLoad) c.onLoad.call(this, h); if (cc) h.w.jqmAddClose($(cc, h.w)); e(h); }); } else if (cc) h.w.jqmAddClose($(cc, h.w)); if (c.toTop && h.o) h.w.before('').insertAfter(h.o); (c.onShow) ? c.onShow(h) : h.w.show(); e(h); return F; }, close: function (s) { var h = H[s]; if (!h.a) return F; h.a = F; if (A[0]) { A.pop(); if (!A[0]) L('unbind'); } if (h.c.toTop && h.o) $('#jqmP' + h.w[0]._jqm).after(h.w).remove(); if (h.c.onHide) h.c.onHide(h); else { h.w.hide(); if (h.o) h.o.remove(); } return F; }, params: {} }; var s = 0, H = $.jqm.hash, A = [], ie6 = $.browser.msie && ($.browser.version == "6.0"), F = false, i = $('').css({ opacity: 0 }), e = function (h) { if (ie6) if (h.o) h.o.html('').prepend(i); else if (!$('iframe.jqm', h.w)[0]) h.w.prepend(i); f(h); }, f = function (h) { try { $(':input:visible', h.w)[0].focus(); } catch (_) { } }, L = function (t) { $()[t]("keypress", m)[t]("keydown", m)[t]("mousedown", m); }, m = function (e) { var h = H[A[A.length - 1]], r = (!$(e.target).parents('.jqmID' + h.s)[0]); if (r) f(h); return !r; }, hs = function (w, t, c) { return w.each(function () { var s = this._jqm; $(t).each(function () { if (!this[c]) { this[c] = []; $(this).click(function () { for (var i in { jqmShow: 1, jqmHide: 1 }) for (var s in this[i]) if (H[this[i][s]]) H[this[i][s]].w[i](this); return F; }); } this[c].push(s); }); }); }; })(jQuery);
62 |
63 | /*
64 | * Viewport - jQuery selectors for finding elements in viewport
65 | *
66 | * Copyright (c) 2008-2009 Mika Tuupola
67 | *
68 | * Licensed under the MIT license:
69 | * http://www.opensource.org/licenses/mit-license.php
70 | *
71 | * Project home:
72 | * http://www.appelsiini.net/projects/viewport
73 | *
74 | */
75 | /* with additions by rchern */
76 | (function ($) { $.belowthefold = function (element, settings) { var fold = $(window).height() + $(window).scrollTop(); return fold <= $(element).offset().top - settings.threshold; }; $.abovethetop = function (element, settings) { var top = $(window).scrollTop(); return top >= $(element).offset().top + $(element).height() - settings.threshold; }; $.rightofscreen = function (element, settings) { var fold = $(window).width() + $(window).scrollLeft(); return fold <= $(element).offset().left - settings.threshold; }; $.leftofscreen = function (element, settings) { var left = $(window).scrollLeft(); return left >= $(element).offset().left + $(element).width() - settings.threshold; }; $.inviewport = function (element, settings) { return !$.rightofscreen(element, settings) && !$.leftofscreen(element, settings) && !$.belowthefold(element, settings) && !$.abovethetop(element, settings); }; $.topVisible = function (element, settings) { var top = $(window).scrollTop(); var bottom = top + $(window).height(); var elPos = $(element).offset().top - settings.threshold; return (top <= elPos) && (bottom >= elPos); }; $.bottomVisible = function (element, settings) { var top = $(window).scrollTop(); var bottom = $(window).height() + $(window).scrollTop(); var elPos = $(element).offset().top + $(element).height() - settings.threshold; return (top <= elPos) && (bottom >= elPos); }; $.leftVisible = function (element, settings) { var left = $(window).scrollLeft(); var right = left + $(window).width(); var elPos = $(element).offset().left - settings.threshold; return (left <= elPos) && (right >= elPos); }; $.rightVisible = function (element, settings) { var left = $(window).scrollLeft(); var right = left + $(window).width(); var elPos = $(element).offset().left + $(element).width() - settings.threshold; return (left <= elPos) && (right >= elPos); }; $.fullyVisible = function (element, settings) { return $.topVisible(element, settings) && $.bottomVisible(element, settings) && $.leftVisible(element, settings) && $.rightVisible(element, settings); }; $.extend($.expr[':'], { "below-the-fold": function (a, i, m) { return $.belowthefold(a, { threshold: 0 }); }, "above-the-top": function (a, i, m) { return $.abovethetop(a, { threshold: 0 }); }, "left-of-screen": function (a, i, m) { return $.leftofscreen(a, { threshold: 0 }); }, "right-of-screen": function (a, i, m) { return $.rightofscreen(a, { threshold: 0 }); }, "in-viewport": function (a, i, m) { return $.inviewport(a, { threshold: 0 }); }, "top-visible": function (a, i, m) { return $.topVisible(a, { threshold: 0 }); }, "bottom-visible": function (a, i, m) { return $.bottomVisible(a, { threshold: 0 }); }, "left-visible": function (a, i, m) { return $.leftVisible(a, { threshold: 0 }); }, "right-visible": function (a, i, m) { return $.rightVisible(a, { threshold: 0 }); }, "fully-visible": function (a, i, m) { return $.fullyVisible(a, { threshold: 0 }); } }); })(jQuery);
77 |
78 | /* shortcut plugin */
79 | (function ($) { $.fn.shortcut = function (fn, params) { params = $.extend({}, $.fn.shortcut.params, params); return this.each(function () { $(this).bind('keyup', function (event) { if (this !== event.target && (/textarea|select/i.test(event.target.nodeName) || event.target.type === "text")) { return; } if (event.keyCode === params.code[params.step]) { if (params.step === 0) { params.startTime = new Date(); } params.step++; } else { params.step = 0; } if (params.step === params.code.length) { if (new Date() - params.startTime <= 2E3) { fn(); } params.step = 0; } }); }); }; $.fn.shortcut.params = { 'code': [38, 38, 40, 40, 37, 39, 37, 39, 66, 65], 'step': 0 }; })(jQuery);
80 |
81 | $(function () {
82 | var curIndex = -2, selectedItem = null;
83 |
84 | var goToDestination = function (url) { location = url; };
85 |
86 | var selectItem = function (items, i) {
87 | if (i < 0) {
88 | // make a guess at the selected post
89 | var fullyVisiblePosts = items.filter(":fully-visible");
90 | if (fullyVisiblePosts.length > 0) {
91 | i = items.index(fullyVisiblePosts[0]);
92 | } else {
93 | var partiallyVisiblePosts = items.filter(":in-viewport");
94 | if (partiallyVisiblePosts.length === 1) {
95 | i = items.index(partiallyVisiblePosts[0]);
96 | } else {
97 | if (i === -1) {
98 | i = items.index(partiallyVisiblePosts[1]);
99 | } else {
100 | i = items.index(partiallyVisiblePosts[0]);
101 | }
102 | }
103 | }
104 | } else {
105 | i = i % items.length;
106 | }
107 | selectedItem = items.css("padding", "5px").css("border", "1px solid " + $("#content").css("backgroundColor")).eq(i);
108 | selectedItem.css("border", "1px dashed black");
109 | if (!selectedItem.is(":fully-visible")) {
110 | $.scrollTo(selectedItem);
111 | }
112 | curIndex = i;
113 | };
114 |
115 | var getCharacterCode = function (code) {
116 | var c = null;
117 | switch (code) {
118 | case "ENTER":
119 | c = 13;
120 | break;
121 | case "ESC":
122 | c = 27;
123 | break;
124 | case "?":
125 | c = 191;
126 | break;
127 | default:
128 | c = code.charCodeAt(0);
129 | break;
130 | }
131 | return c;
132 | };
133 |
134 | var convertCode = function (code) {
135 | var chars = code.split(",");
136 | for (var i = 0; i < chars.length; i++) { chars[i] = getCharacterCode(chars[i]); }
137 | return chars;
138 | };
139 |
140 | var GlobalShortcuts = {
141 | "home": { code: "G,H", url: "/" },
142 | "questions": { code: "G,Q", url: "/questions" },
143 | "tags": { code: "G,T", url: "/tags" },
144 | "users": { code: "G,U", url: "/users" },
145 | "badges": { code: "G,B", url: "/badges" },
146 | "unanswered": { code: "G,N", url: "/unanswered" },
147 | "ask": { code: "G,A", url: "/questions/ask" },
148 | "recent": { code: "G,E", url: "/users/recent" },
149 | "rep audit": { code: "G,R", url: "/reputation" },
150 | "chat": { code: "G,C", url: "http://chat." + location.hostname },
151 | "profile": { code: "G,P", url: $("#hlinks-user a").attr("href") },
152 | "profile summary": { code: "G,S", fn: function () { $(".profile-triangle").mouseover(); } },
153 | "main vs meta": { code: "G,M", url: "http://" + (location.hostname.indexOf("meta") === 0 ? location.hostname.substring(5) : "meta." + location.hostname) },
154 | "inbox": { code: "G,I", fn: function () { $("#portalLink .genu").click(); $("#portalLink #seTabInbox").click(); } },
155 | "help": { code: "?", fn: function () { $("#helpOverlay").jqmShow(); } },
156 | "close help": { code: "ESC", fn: function () { $(".jqmWindow").jqmHide(); } }
157 | };
158 |
159 | var setupHelpOverlay = function () {
160 | var styleText = ""; // ".easy-navigation-peekable { -moz-border-radius: 4px 4px 4px 4px;background-color: #000000;border-radius: 4px 4px 4px 4px;color: #F0F0F0;margin-top: 5px;padding-right: 0px !important;position: absolute;z-index: 4;} .easy-navigation-peeked-message: {line-height: 1.5em;padding: 3px 0px 3px 15px;},.easy-navigation-subtle: { color: #D2F7D0;display: block;font-size: 10px;}";
161 | styleText += ".jqmWindow {display: none;position: fixed;top: 17%;left: 50%;margin-left: -300px;width: 600px;background-color: #EEE;color: #333;border: 1px solid black;padding: 12px;}.jqmOverlay { background-color: #000; } * iframe.jqm {position:absolute;top:0;left:0;z-index:-1;width: expression(this.parentNode.offsetWidth+'px');height: expression(this.parentNode.offsetHeight+'px');}* html .jqmWindow {position: absolute;top: expression((document.documentElement.scrollTop || document.body.scrollTop) + Math.round(17 * (document.documentElement.offsetHeight || document.body.clientHeight) / 100) + 'px');}";
162 | $('').text(styleText).appendTo('head');
163 | var helpOverlay = $("");
164 | var text = "";
165 | $.each(GlobalShortcuts, function (name, item) {
166 | text += "
" + name + ": " + item.code;
167 | });
168 | var globalOverlay = $("
Global Shortcuts:" + text + "
").appendTo(helpOverlay);
169 | text = "";
170 | $.each(PostShortcuts, function (name, item) {
171 | text += "
" + name + ": " + item.code;
172 | });
173 | var postOverlay = $("
Post Shortcuts:" + text + "
").appendTo(helpOverlay);
174 |
175 | text = "";
176 | $.each(ListingShortcuts, function (name, item) {
177 | text += "
" + name + ": " + item.code;
178 | });
179 | var listingOverlay = $("
Listing Shortcuts:" + text + "
").appendTo(helpOverlay);
180 |
181 | helpOverlay.appendTo(document.body).jqm();
182 | };
183 |
184 | var PostShortcuts = {
185 | "vote up": { code: "V,U", fn: function () { selectedItem.closest("#question,.answer").find(".vote-up-off").click(); } },
186 | "vote down": { code: "V,D", fn: function () { selectedItem.closest("#question,.answer").find(".vote-down-off").click(); } },
187 | "reply / comment": { code: "R", fn: function () { selectedItem.closest("#question,.answer").find("a.comments-link").click(); } },
188 | "flag": { code: "F", fn: function () { selectedItem.closest("#question,.answer").find("a[id^=flag]").click(); } },
189 | "close": { code: "V,C", fn: function () { selectedItem.closest("#question,.answer").find("a[id^=close]").click(); } },
190 | "edit": { code: "E", fn: function () { selectedItem.closest("#question,.answer").find("a[href=edit]").click(); } },
191 | "bounty": { code: "B", fn: function () { selectedItem.closest("#question,.answer").find("#bounty-link").click(); } },
192 | "owner": { code: "O", fn: function () { goToDestination(selectedItem.closest("#question,.answer").find(".user-details a").attr("href")); } }
193 | };
194 |
195 | var ListingShortcuts = {
196 | "go to question": { code: "ENTER", fn: function () { goToDestination("/questions/" + selectedItem.attr("id").replace("question-summary-", "")); } }
197 | };
198 |
199 |
200 | var $w = $(window);
201 | $.each(GlobalShortcuts, function (name, item) {
202 | code = convertCode(item.code);
203 | $w.shortcut(function () {
204 | if (item.url) { goToDestination(item.url); }
205 | else { item.fn(); }
206 | }, { "code": code });
207 | });
208 |
209 | var questionListing = $("#questions,#question-mini-list").length > 0;
210 | if (questionListing) {
211 | var questions = $("div.question-summary");
212 | $w.shortcut(function () { curIndex++; selectItem(questions, curIndex); }, { "code": convertCode("J") });
213 | $w.shortcut(function () { curIndex--; selectItem(questions, curIndex); }, { "code": convertCode("K") });
214 | $.each(ListingShortcuts, function (name, item) {
215 | code = convertCode(item.code);
216 | $w.shortcut(function () { item.fn(); }, { "code": code });
217 | });
218 | } else {
219 | var posts = $("div.post-text");
220 | $w.shortcut(function () { curIndex++; selectItem(posts, curIndex); }, { "code": convertCode("J") });
221 | $w.shortcut(function () { curIndex--; selectItem(posts, curIndex); }, { "code": convertCode("K") });
222 |
223 | $.each(PostShortcuts, function (name, item) {
224 | code = convertCode(item.code);
225 | $w.shortcut(function () { item.fn(); }, { "code": code });
226 | });
227 | }
228 | setupHelpOverlay();
229 | });
230 | });
--------------------------------------------------------------------------------
/SEChatModifications.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name SE Chat Modifications
3 | // @description A collection of modifications for SE chat rooms
4 | // @match *://chat.meta.stackexchange.com/rooms/*
5 | // @match *://chat.stackoverflow.com/rooms/*
6 | // @match *://chat.stackexchange.com/rooms/*
7 | // @author @rchern
8 | // ==/UserScript==
9 |
10 | /*
11 | * Injects functions into the page so they can freely interact with existing code
12 | */
13 | function inject() {
14 | for (var i = 0; i < arguments.length; ++i) {
15 | if (typeof(arguments[i]) == 'function') {
16 | var script = document.createElement('script');
17 |
18 | script.type = 'text/javascript';
19 | script.textContent = 'if (window.jQuery) (' + arguments[i].toString() + ')(window.jQuery)';
20 |
21 | document.body.appendChild(script);
22 | }
23 | }
24 | }
25 |
26 | // Inject the support plugins, followed by the main userscript function
27 | inject(livequery, bindas, expressions, function ($) {
28 | // Setup the selector shortcuts
29 | var Selectors = {
30 | 'getMessage': function getMessage(id) {
31 | if (id)
32 | validate('number');
33 |
34 | return id ? '#message-' + id : '.user-container.mine:last .message:last';
35 | },
36 | 'getSignature': function getSignature(match) {
37 | validate('string');
38 |
39 | return ".signature:contains('" + match + "') ~ .messages";
40 | },
41 | 'getRoom': function getRoom(match) {
42 | validate('string');
43 |
44 | return "#my-rooms > li > a[href^='/rooms']:contains('" + match + "')";
45 | }
46 | };
47 |
48 | // Setup the highlight and clipping objects
49 | var Highlights = new Storage(),
50 | Clippings = new Storage('chatClips');
51 |
52 | // The list of command states that can be returned from a command function, or one of the command utility functions
53 | var CommandState = {
54 | // The command wasn't found
55 | 'NotFound': -1,
56 | // The command failed validation or couldn't execute properly
57 | 'Failed': 0,
58 | // The command succeeded, and the input should be cleared (where applicable)
59 | 'SucceedDoClear': 1,
60 | // The command succeeded, and the input should not be cleared (where applicable)
61 | 'SucceedNoClear': 2
62 | };
63 |
64 | // Setup the command and onebox mappings
65 | var Commands = {}, Oneboxes = {};
66 |
67 | // Create the navigation
68 | var Navigation = new Navigation();
69 |
70 | // Setup the main chat extension function, responsible for handling command processing (client-exposed)
71 | var ChatExtension = window.ChatExtension = new function () {
72 | /*
73 | * Defines new chat extension commands, allowing outside functions to plug into the existing userscript infrastructure
74 | */
75 | this.define = function (name, fn, help) {
76 | name = name.toLowerCase();
77 |
78 | if (typeof fn !== 'function')
79 | throw new Error("The function assigned to " + name + " is not a function");
80 |
81 | if (Commands[name])
82 | throw new Error("The command " + name + " is already defined");
83 |
84 | if (help && typeof help === 'string')
85 | fn.helptext = help;
86 |
87 | Commands[name] = fn;
88 | };
89 |
90 | /*
91 | * Associates domains with functions that produce pseudo-oneboxes
92 | */
93 | this.associate = function (domain, fn) {
94 | if (typeof domain === 'string') {
95 | var assignment = domain.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+).*/i);
96 |
97 | if (!assignment)
98 | throw new Error("The domain " + domain + " does not look valid");
99 |
100 | domain = assignment[1].toLowerCase();
101 |
102 | if (Oneboxes[domain])
103 | throw new Error("The domain " + domain + " is already onebox-associated");
104 |
105 | Oneboxes[domain] = fn;
106 | } else if (domain instanceof RegExp) {
107 | if (!Oneboxes['_regex'])
108 | Oneboxes['_regex'] = [];
109 |
110 | Oneboxes['_regex'].push({
111 | 'pattern': domain,
112 | 'handler': fn
113 | });
114 | } else {
115 | throw new Error("The provided domain is not an acceptable type");
116 | }
117 | }
118 |
119 | /*
120 | * Executes commands and automatically displays errors in the case of failed function validation
121 | */
122 | this.execute = function (name, args) {
123 | var result = CommandState.NotFound;
124 |
125 | // Check if the command is defined
126 | if (Commands[name]) {
127 | try {
128 | // Attempt to run the command and get the result
129 | result = Commands[name].apply(this, args);
130 | } catch (ex) {
131 | if (ex.message)
132 | ChatExtension.notify(ex.message);
133 |
134 | result = CommandState.Failed;
135 | }
136 | }
137 |
138 | return result;
139 | };
140 |
141 | /*
142 | * Displays a (usually error) notification dialog to the user for the specified period of time, or until a keyboard or mouse press event occurs
143 | */
144 | this.notify = function (message, delay) {
145 | if (!delay)
146 | delay = 3000;
147 |
148 | $('#inputerror').html(message)
149 | .clearQueue()
150 | .fadeIn("slow")
151 | .delay(delay)
152 | .fadeOut("slow")
153 | .hover(
154 | function () {
155 | $(this).clearQueue();
156 | },
157 | function () {
158 | $(this).delay(delay).fadeOut("slow");
159 | }
160 | )
161 | .css({
162 | 'max-height': ($(window).height() - 90) + 'px',
163 | 'max-width': '60%'
164 | });
165 | };
166 |
167 | /*
168 | * Adds styles to the userscript's