aboutsummaryrefslogtreecommitdiffstats
path: root/gvimail.js
blob: c3b765849a33d6abd7949a3d7c0c1f37772ba746 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
 *
 * ==VimperatorPlugin==
 * @name           GViMail
 * @description    Make Gmail behave like Vim
 * @author         Mahefa Randimbisoa (DotMG) <dotmg@users.sourceforge.net>
 * @license        GPL 2.0
 * @requires       Vimperator 2.3pre
 * @url            http://code.google.com/p/gvimail/
 * @version        0.1
 * ==/VimperatorPlugin==
 *
 * Mappings:
 *  zf: re-give focus to the main frame.
 *  zR: Opens all folds.
 *  zM: Closes all folds.
 */
(function(){
	// Set use_gmail_v1 to false if you don't use the older version of Gmail at all.
	// Support of the older version of Gmail is very limited.
	var use_gmail_v1 = true;
	// Set use_gmail_v2 to false if you don't use the version 2 of Gmail.
	var use_gmail_v2 = true;
	// If you have installed stylechanger.js and want to use the Vimish style, set the option below to true.
	// If you didn't yet get the gvimail.css, download it from http://code.google.com/p/gvimail/source/browse/trunk/colors/GVimail.css
	// and put it in ~/.vimperator/colors (or %HOMEPATH\vimperator\colors)
	var use_gvimail_css = false;


	var GViMail = {
	modes:config.browserModes || [modes.NORMAL],
	/// get the iframe object that contains all the visible items. This should always get focus.
	getMainCanvas : function()
	{ // It is simpler to find the main canvas in Gmail v2, (id='canvas_frame')
		var canvas_frame = window.content.document.getElementById('canvas_frame');
		if (canvas_frame) return (canvas_frame);
		if (use_gmail_v1)
		{// On older versions of Gmail, The main canvas is the iframe that has the attribute left: 0pt
			return (util.evaluateXPath('//iframe[contains(@style, "left: 0pt")]', window.content.frames[0].document, null, true).iterateNext());
		}
		return null;
	},
	/// Execute an action by simulating a click on an image. It has 2 params: the classname (for Gmail v2)
	/// or a part of the src attribute of the img element (for Gmail v1)
	clickImage : function (classnamev2, imgsrcv1)
	{
		var elem = util.evaluateXPath('//*[contains(concat(" ", @class, " "), " '+classnamev2+' ")] | //img[contains(@src, "'+imgsrcv1+'")]', GViMail.getMainCanvas().contentDocument, null, true).iterateNext();
		// hmm, the code below generates a log: Invalid argument for followLink.
		buffer.followLink(elem, liberator.CURRENT_TAB);
	},
	/// Gives focus to the main Canvas, to make all keys working well.
	focusMainFrame:function ()
	{
		GViMail.getMainCanvas().contentWindow.focus();
	},
	/// On TabSelect (if Gmail Tab), we will give focus to the main canvas.
	get isGmail () (/^https?:\/\/mail\.google\.com\//.test(buffer.URL)),
	/// when you type some key to make an action, habitually, the main canvas looses focus.
	/// we will add an EventListener on keypress to avoid this.
	preventLooseFocus:function()
	{
		if (liberator.mode == modes.NORMAL
				&& !liberator.mode.isRecording
				&& !(modes.extended & modes.MENU)
				&& !modes.passNextKey
				&& !modes.passAllKeys)
		{
			GViMail.focusMainFrame();
		}
	}
};

mappings.addUserMap(GViMail.modes, ["zM"],
	"Closes all fold",
	function () { GViMail.clickImage('Dm2exe', 'collapse_icon'); });
mappings.addUserMap(GViMail.modes, ["zR"],
	"Opens all fold",
	function () { GViMail.clickImage('kPoXId', 'expand_icon'); });
// Let's build the Gmail(v2)-custom hinttags. Follow the comments to understand.
var gmail_v2_hinttags =
	"//span[@selector]"
	// Menu Settings, Older version, Compose Mail, Inbox, Starred .. Contacts, Labels, turn on/off chat
	// The [not(ancestor::tr//td[@class='mka4te'])] is to avoid interferring with //td[@class='mka4te']/ancestor::tr/td[5], see Select message in the list below
	+ " | //span[@role='link'][not(ancestor::tr//td[@class='mka4te'])]"
	// Refresh, Back to "label", Reply to all, Forward, Filter messages like this, ...
	// You could just use //div[@act] here, but there appears 4 unwanted hints when first-viewing a message
	+ " | //div[@act][not(ancestor::div[contains(concat(' ', @class, ' '), ' zWKgkf ')]) or (ancestor::div[contains(concat(' ', @class, ' '), ' zWKgkf ') and contains(@style, 'visibility')])]"
	// More actions, Toolbar buttons on RTE (don't use RTE, plain ascii mails are sexier)
	+ " | //*[@unselectable='on']"
	// Fold and UnFold messages in thread that has an excerpt displayed in grey
	+ " | //div[contains(concat(' ', @class, ' '), ' IUCKJe ')]"
	// UnFold messages in thread when no excerpt is displayed (blank line)
	//    Such <div>s have a class XoqCub, have another <div> child having the class YrHFdf, and there is no table il all their descendants
	+ " | //*[contains(concat(' ', @class, ' '), ' XoqCub ')]/div[@class='YrHFdf'][count(descendant-or-self::table)=0]"
	// Star on message list
	+ " | //td[@class='mka4te']/img"
	// Star on thread list (same subject)
	+ " | //td/span[starts-with(@class, 'lHQn1d')]/img"
	// Delete all spam messages now
	+ " | //*[@class='rj1J6b'"
	// Invite x@y.z to Gmail.
	+ " or @class='YCDlS'"
	// When you delete any message in a thread view, there are links saying "n deleted messages in this conversation. View message or delete forever."
	+ " or @class='u1T3K' or @class='iVE0ue'"
	// Hide filter options (settings)
	+ " or @class='u7uAnb']"
	// Select message in the list
	+ " | //td[@class='mka4te']/ancestor::tr/td[5]"
	// Change picture [Settings] ==> next step still not working
	+ " | //div[@class='c3pyI']/span"
	// Attach a file + Add event invitation + Rich formatting|Plain text
	+ " | //*[contains(concat(' ', @class, ' '), ' MRoIub ')]"
	// Check spelling
	+ " | //span[@class='mrKIf']"
	// Everything that is displayed as image (+ Edit labels)
	+ " | //img[contains(@src, 'cleardot.gif')]"
	// Reply + Reply to all + Forward + show/hide details + Edit labels
	//    We will not select divs that contains any hintable elements inside
	+ " | //*[@idlink][count(descendant-or-self::span[@role='link'])=0 and count(descendant-or-self::a)=0]"
	// <label>|x
	+ " | //table[@class='Ir5Jyf']//span"
	// Settings> Accounts> make_default|edit_info|delete|View_history|Check_mail_now
	+ " | //*[contains(concat(' ', @class, ' '), ' GaVz0 ')]"
	// Update conversation, Ignore (when someone just posted a message on the thread you're reading & editing)
	+ " | //*[contains(concat(' ', @class, ' '), ' Gf76kb ')]"
	+ " | //*[contains(concat(' ', @class, ' '), ' GRpVjf ')]" //Recently changed to this ...
	// Show|Hide quoted text
	+ " | //span[contains(concat(' ', @class, ' '), ' WQ9l9c ')]"
	//
	+ " | //div[contains(concat(' ', @class), ' goog-menuitem')]";
// We provide limited support for Gmail(v1)
var gmail_v1_hinttags =
      "//*[contains(@class, 'lk ') or @class='msc' or @class='ll' or @class='setl' or @class='lkw' or starts-with(@class, 'sc ')] | //tr[@class='rr' or @class='ur']/td[position()=5] | //div/span[contains(@class, 'bz_rbbb')] | //span[@class='l' and contains(@id, 'sl_')]" ;
var gmail_hints = use_gmail_v1 ? gmail_v1_hinttags : "";
if (use_gmail_v2) gmail_hints = gmail_hints + (gmail_hints ? " | " : "") + gmail_v2_hinttags;
gmail_hints = gmail_hints + (gmail_hints ? " | " : "") + options['hinttags'];

// Now: override default hinttags. Override is not the true wording, I'd rather say extend.
/*options.add(["hinttags", "ht"],
	"XPath string of hintable elements activated by 'f' and 'F'",
	// Gmail uses span[@selector] for labels in the line Select: All, None, Read, Unread, Starred, Unstarred
	"string",
	gmail_hints);*/
// This is not the most elegant solution to do this, but I don't manage to find the correct one myself...
options.get("hinttags").value=gmail_hints;

// When navigation keys (and others) no longer work, type zf to focus to the main frame
mappings.addUserMap(GViMail.modes, ["zf"],
	"Focus main frame",
	function () { GViMail.focusMainFrame(); });
getBrowser().mTabBox.addEventListener('TabSelect', function(event){
	if (GViMail.isGmail)
		window.setTimeout(function(){GViMail.focusMainFrame();}, 100);
 }, false);
window.addEventListener('keypress', function () {
	if (GViMail.isGmail)
		GViMail.preventLooseFocus();
}, true);
if (use_gvimail_css && (typeof liberator.globalVariables.styles == 'undefined' || liberator.globalVariables.styles == ''))
{
	liberator.globalVariables.styles = 'style,gvimail';
}

})();
// vim:noet:
class="nx">tag search by keyword page. Following special tokens are available. default setting is "$SERVICENAME : $KEYWORD($NUMOFTAGS件) - $URL $COMMENT". $SERVICENAME: the value of "matanico_related_tag_servicename". $KEYWORD: search keyword. $NUMOFVIDEOS: the number of search result. $URL: search result URL. $COMMENT: comment. if this token is not specified, ex-command ":matanico" ignore any comments. matanico_related_tag_servicename: the token "$SERVICENAME" specified by "matanico_related_tag_format" will be expanded to this value. this variable facilitate to change posted string without modefication of format. default value is "またキーワードでニコニコタグ検索してる". ]]></detail> <detail lang="ja"><![CDATA[ == EX-COMMANDS == :matanico [comment]: 今見ている動画 / 検索した結果の情報を Twitter に投稿します comment はなくてもかまいません :matanico! [comment]: Twitter に投稿される文字列をクリップボードにコピーします Twitter には投稿しません == SETTINGS == matanico_watch_format: 動画ページにおいて投稿される文章の書式設定です以下の特殊な文字列を指定可能です設定なしの場合 "$SERVICENAME : $SUBJECT($PLAYTIME) - $URL $COMMENT" になります $SERVICENAME: matanico_watch_servicename で指定した文字列に展開されます $SUBJECT: 動画の名前に展開されます $PLAYTIME: 再生時間に展開されます $URL: 動画の URL に展開されます $COMMENT: コメントに展開されますこの指定がないと :matanico コマンドでコメントを書いても反映されません matanico_watch_servicename: matanico_watch_format で指定した $SERVICENAME 部分がこの値で展開されます書式はそのままで投稿する文字列のみを変更したい場合にこの値を変更することで設定が容易になります設定なしの場合 "またニコニコ動画見てる" が使用されます matanico_live_format: 生放送ページにおいて投稿される文章の書式設定です以下の特殊な文字列を指定可能です設定なしの場合 "$SERVICENAME : $SUBJECT - $URL $COMMENT" になります $SERVICENAME: matanico_live_servicename で指定した文字列に展開されます $SUBJECT: 動画の名前に展開されます $URL: 動画の URL に展開されます $COMMENT: コメントに展開されますこの指定がないと :matanico コマンドでコメントを書いても反映されません matanico_live_servicename: matanico_live_format で指定した $SERVICENAME 部分がこの値で展開されます書式はそのままで投稿する文字列のみを変更したい場合にこの値を変更することで設定が容易になります設定なしの場合 "またニコニコ生放送見てる" が使用されます matanico_tag_format: タグ検索ページにおいて投稿される文章の書式設定です以下の特殊な文字列を指定可能です設定なしの場合 "$SERVICENAME : $TAG($NUMOFVIDEOS件) - $URL $COMMENT" が使用されます $SERVICENAME: matanico_tag_servicename で指定した文字列に展開されます $TAG: 検索したタグ名に展開されます複数指定の場合半角スペースで区切られます $NUMOFVIDEOS: 検索結果の件数に展開されます $URL: 検索結果の URL に展開されます $COMMENT: コメント展開されますこの指定がないと :matnico コマンドでコメントを書いても反映されません matanico_tag_servicename: matanico_tag_format で指定した $SERVICENAME 部分がこの値で展開されます書式はそのままで投稿する文字列のみを変更したい場合にこの値を変更することで設定が容易になります設定なしの場合 "またニコニコタグ検索してる" が使用されます matanico_related_tag_format: キーワードによるタグ検索ページにおいて投稿される文章の書式設定です以下の特殊な文字列を指定可能です設定なしの場合 "$SERVICENAME : $KEYWORD($NUMOFTAGS件) - $URL $COMMENT" が使用されます $SERVICENAME: matanico_related_tag_servicename で指定した文字列に展開されます。。 $KEYWORD: 検索したキーワードに展開されます $NUMOFVIDEOS: 検索結果の件数に展開されます $URL: 検索結果の URL に展開されます $COMMENT: コメント展開されますこの指定がないと :matnico コマンドでコメントを書いても反映されません matanico_related_tag_servicename: matanico_related_tag_format で指定した $SERVICENAME 部分がこの値で展開されます書式はそのままで投稿する文字列のみを変更したい場合にこの値を変更することで設定が容易になります設定なしの場合 "またキーワードでニコニコタグ検索してる" が使用されます ]]></detail> </VimperatorPlugin>; (function () { // class definitions // change XPath query when html changed. function NicoScraper() {} NicoScraper.prototype = { _constants: { VERSION: '0.73', PAGECHECK: [ ['live', '^http://live\\.nicovideo\\.jp/watch/'], ['watch', '^http://[^.]+\\.nicovideo\\.jp/watch/'], ['tag', '^http://[^.]+\\.nicovideo\\.jp/tag/'], ['relatedTag', '^http://[^.]+\\.nicovideo\\.jp/related_tag/'], ], }, version: function () { return this.constants.VERSION; }, pagecheck: function () { const pageCheckData = this._constants.PAGECHECK; const currentURL = this.getURL(); for each (let [name, url] in pageCheckData) { if (currentURL.match(url)) return name; } throw new Error('current tab is not nicovideo.jp'); }, _flvplayer: function () { if (this.pagecheck() === 'watch') { let flvplayer = window.content.document.getElementById('flvplayer'); if (!flvplayer) throw new Error('video player is not found'); return flvplayer.wrappedJSObject || flvplayer; } throw new Error('current tab is not watch page on nicovideo.jp'); }, getURL: function () { return liberator.modules.buffer.URL; }, getSubject: function () { let subjectNode; switch (this.pagecheck()) { case 'watch': subjectNode = $f('id("des_2")/table/tbody/tr/td/h1'); break; case 'live': subjectNode = $f('id("stream_description")'); break; default: break; } if (subjectNode) return subjectNode.textContent; throw new Error('current tab is not watch page on nicovideo.jp'); }, getPlaytime: function () { let p = this._flvplayer(); if (p && p.ext_getTotalTime) { let playtime = Math.round(p.ext_getTotalTime()); let min = Math.floor(playtime / 60); let sec = playtime % 60; if (sec < 10) sec = '0' + sec; return [min, sec].join(':'); } throw new Error('current tab is not watch page on nicovideo.jp'); }, getTagName: function () { if (this.pagecheck() === 'tag') { let wordNodes = liberator.modules.util.evaluateXPath('id("search_words")/span[contains(concat(" ", @class, " "), " search_word ")]'); let words = []; for (let wordNode in wordNodes) words.push(wordNode.textContent); return words.join(' '); } throw new Error('current tab is not tag search page on nicovideo.jp'); }, getNumofVideos: function () { if (this.pagecheck() === 'tag') { let numofVideos = $f('//strong[contains(concat(" ", @class, " "), " result_total ")]'); return numofVideos.textContent; } throw new Error('current tab is not tag search page on nicovideo.jp'); }, getKeyword: function () { if (this.pagecheck() === 'relatedTag') { let keyword = $f('//strong[contains(concat(" ", @class, " "), " search_word ")]'); return keyword.textContent; } throw new Error('current tab is not related tag search page on nicovideo.jp'); }, getNumofTags: function () { if (this.pagecheck() === 'relatedTag') { let numofTags = $f('//strong[contains(concat(" ", @class, " "), " result_total ")]'); return numofTags.textContent; } throw new Error('current tab is not related tag search page on nicovideo.jp'); }, }; function TwitterUpdaterFactory() { let pUsername, pPassword; let pEndPoint = 'https://twitter.com/statuses/update.json'; function TwitterUpdater() {} TwitterUpdater.prototype.update = function (data) { let parameter = 'status=' + encodeURIComponent(data.newStatus); let req = new XMLHttpRequest(); if (req) { req.open('POST', pEndPoint, true, pUsername, pPassword); req.onreadystatechange = function () { if (req.readyState === 4 && req.status === 200) { data.onSuccess(); return; } throw new Error('failure to update status in Twitter. HTTP status code : ' + req.status); } req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); req.send(parameter); } }; TwitterUpdater.prototype.__defineSetter__( 'username', function (username) { pUsername = username; return this; } ); TwitterUpdater.prototype.__defineSetter__( 'password', function (password) { pPassword = password; return this; } ); return new TwitterUpdater(); } // main --- let scraper = new NicoScraper(); liberator.modules.commands.addUserCommand( ['matanico'], 'update Twitter status to current video/search page information and comment', function (args) { let arg = args.string; try { // build post string ----- let postString; // domain check switch(scraper.pagecheck()) { case 'watch': postString = onWatch(scraper, arg); break; case 'live': postString = onLive(scraper, arg); break; case 'tag': postString = onTagSearch(scraper, arg); break; case 'relatedTag': postString = onRelatedTagSearch(scraper, arg); break; default: throw new Error('current tab is not nicovideo.jp'); break; } // ':matanico!' display the evaluated format. if (args.bang) { liberator.modules.util.copyToClipboard(postString, true); return; } // get username/password and set TwitterUpdater let t = TwitterUpdaterFactory(); [t.username, t.password] = getUserAccount({ hostname: 'http://twitter.com/', formSubmitURL: 'https://twitter.com/statuses/update.json', httpRealm: null, description: 'Enter e-mail address and password. This information is cached and use from next time.', }); t.update({ newStatus: postString, onSuccess: function () liberator.echo('Posted ' + postString), }); } catch (e) { liberator.echoerr(e); liberator.log(e); } }, { bang: true, }, true ); // sub function onWatch(scraper, comment) { let format = liberator.globalVariables.matanico_watch_format || '$SERVICENAME : $SUBJECT($PLAYTIME) - $URL $COMMENT'; let serviceName = liberator.globalVariables.matanico_watch_servicename || fromUTF8Octets('またニコニコ動画見てる'); return format.replace(/\$SERVICENAME/g, serviceName) .replace(/\$SUBJECT/g, scraper.getSubject()) .replace(/\$PLAYTIME/g, scraper.getPlaytime()) .replace(/\$URL/g, scraper.getURL()) .replace(/\$COMMENT/g, comment); } function onLive(scraper, comment) { let format = liberator.globalVariables.matanico_live_format || '$SERVICENAME : $SUBJECT - $URL $COMMENT'; let serviceName = liberator.globalVariables.matanico_live_servicename || fromUTF8Octets('またニコニコ生放送見てる'); return format.replace(/\$SERVICENAME/g, serviceName) .replace(/\$SUBJECT/g, scraper.getSubject()) .replace(/\$URL/g, scraper.getURL()) .replace(/\$COMMENT/g, comment); } function onTagSearch(scraper, comment) { let format = liberator.globalVariables.matanico_tag_format || fromUTF8Octets('$SERVICENAME : $TAG($NUMOFVIDEOS件) - $URL $COMMENT'); let serviceName = liberator.globalVariables.matanico_tag_servicename || fromUTF8Octets('またニコニコタグ検索してる'); return format.replace(/\$SERVICENAME/g, serviceName) .replace(/\$TAG/g, scraper.getTagName()) .replace(/\$NUMOFVIDEOS/g, scraper.getNumofVideos()) .replace(/\$URL/g, scraper.getURL()) .replace(/\$COMMENT/g, comment); } function onRelatedTagSearch(scraper, comment) { let format = liberator.globalVariables.matanico_related_tag_format || fromUTF8Octets('$SERVICENAME : $KEYWORD($NUMOFTAGS件) - $URL $COMMENT'); let serviceName = liberator.globalVariables.matanico_related_tag_servicename || fromUTF8Octets('またキーワードでニコニコタグ検索してる'); return format.replace(/\$SERVICENAME/g, serviceName) .replace(/\$KEYWORD/g, scraper.getKeyword()) .replace(/\$NUMOFTAGS/g, scraper.getNumofTags()) .replace(/\$URL/g, scraper.getURL()) .replace(/\$COMMENT/g, comment); } // stuff functions function $f(query, node) { node = node || window.content.document; let result = (node.ownerDocument || node).evaluate( query, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); return result.singleNodeValue || null; } function fromUTF8Octets(octets) decodeURIComponent(octets.replace(/[%\x80-\xFF]/g, function (c) '%' + c.charCodeAt(0).toString(16))); function getUserAccount(accountInfo) { // refer: // https://developer.mozilla.org/ja/Using_nsILoginManager let loginManager = Cc['@mozilla.org/login-manager;1'].getService(Ci.nsILoginManager); let logins = loginManager.findLogins( {}, accountInfo.hostname, accountInfo.formSubmitURL, accountInfo.httpRealm ); if (logins.length > 0) { // found return [logins[0].username, logins[0].password]; } else { // not found, so register // refer: https://developer.mozilla.org/Ja/Code_snippets/Dialogs_and_Prompts let promptSvc = Cc['@mozilla.org/embedcomp/prompt-service;1'].getService(Ci.nsIPromptService); let promptUsername = {value: ''}; let promptPassword = {value: ''}; let isOK = promptSvc.promptUsernameAndPassword( window, accountInfo.hostname, accountInfo.description, promptUsername, promptPassword, null, {} ); if (isOK) { let nsLoginInfo = new Components.Constructor( '@mozilla.org/login-manager/loginInfo;1', Ci.nsILoginInfo, 'init' ); // refer: https://developer.mozilla.org/ja/NsILoginInfo loginManager.addLogin(new nsLoginInfo( accountInfo.hostname, accountInfo.formSubmitURL, accountInfo.httpRealm, promptUsername.value, promptPassword.value, '', '' )); return [promptUsername.value, promptPassword.value]; } } throw new Error('account is not found: ' + accountInfo.hostname); } })(); // vim:sw=4 ts=4 et: