diff options
-rwxr-xr-x | twittperator/twsidebar.tw | 524 | ||||
-rw-r--r-- | twittperator/twsidebar/chrome.manifest | 4 | ||||
-rw-r--r-- | twittperator/twsidebar/chrome/content/overlay.xul | 25 | ||||
-rw-r--r-- | twittperator/twsidebar/chrome/content/twsidebar.css | 50 | ||||
-rw-r--r-- | twittperator/twsidebar/chrome/content/twsidebar.xul | 37 | ||||
-rw-r--r-- | twittperator/twsidebar/chrome/locale/en-US/nosidebar.dtd | 3 | ||||
-rw-r--r-- | twittperator/twsidebar/install.rdf | 18 |
7 files changed, 661 insertions, 0 deletions
diff --git a/twittperator/twsidebar.tw b/twittperator/twsidebar.tw new file mode 100755 index 0000000..47cae8c --- /dev/null +++ b/twittperator/twsidebar.tw @@ -0,0 +1,524 @@ +liberator.modules.TWAnekoSB = ANekoSB = (function () { + + /********************************************************************************* + * Config + *********************************************************************************/ + + let Config = liberator.globalVariables.twittperator_sidebar_config || { + // for Keyword タグ + keyword: /neko|vimp|cat|猫/i, + + // ツイート内に含まれると、表示上抹殺される (reply とか除く + vanish: /うぎぃいいい/i, + + // 自分のスクリーンネーム + screenName: 'anekos', + + // 自分のその他の名前 + myNames: /anekos|悪魔猫将軍/i, + + // ログファイル てけとーなフォーマットで保存されます + //logFile: io.File('~/.chirpstream'), + //myLogFile: io.File('~/.mychirpstream'), + + // 各イベント時に音がなる + sound: { + meow: makeAudio('file:///home/anekos/sound/my/meow.wav'), + fanfare: makeAudio('file://C:/sound-data/fanfare.wav', 0.5), + retweet: makeAudio('file:///home/anekos/sound/my/meow.wav', 0.8), + favorite: makeAudio('file:///home/anekos/sound/my/meow.wav', 0.6), + reply: makeAudio('file:///home/anekos/sound/my/meow.wav', 1.0), + debug: makeAudio('file:///home/anekos/sound/my/meow.wav', 1.0), + filter: makeAudio('file:///home/anekos/sound/my/meow.wav', 1.0), + }, + + // 文字のサイズ + fontSize: 15, + + // リストの最大保持数 + listMax: 100, + + // 日本語だけ for filter stream + jpOnly: true, + + // 地震ツイートの本文に場所をくっつける + earthquake: true, + }; + + // 日本語判定 + JP = new RegExp("[\\u4e00-\\u9fa0\\u30A1-\\u30F6\\u30FC\\u3042-\\u3093\\u3001\\u3002\\uFF01\\uFF1F]"); + + /********************************************************************************* + * Main + *********************************************************************************/ + + // util {{{ + + function className (n) + ('tw-anekos-sb-plugin-' + n); + + function px (n) + parseInt(n, 10) + 'px'; + + // }}} + + function formatText (str) { // {{{ + str = str.trim(); + let reg = /https?:\/\/[^\s]+|[#@]\w+/g; + XML.ignoreWhitespace = false; + let m, i = 0, buf = "", x = <xhtml:p class="twlist-text" xmlns:xhtml="http://www.w3.org/1999/xhtml"/>; + while((m=reg.exec(str))){ + buf = str.substring(i, m.index); + if (buf) + x.appendChild(buf); + let klass = "twlist-link", href = ""; + switch (m[0].charAt(0)){ + case "@": + klass += " twlist-user"; + href = "http://twitter.com/" + m[0].substr(1); + break; + case "#": + klass += " twlist-hash"; + href = "http://twitter.com/search?q=%23" + m[0].substr(1); + break; + default: + klass += " twlist-url"; + href = m[0]; + } + x.appendChild(<xhtml:a class={klass} href={href} + onclick="twlist.onClick(event)" xmlns:xhtml={XHTML}>{m[0]}</xhtml:a>); + i=reg.lastIndex; + } + buf = str.substr(i); + if (buf) + x.appendChild(buf); + return x; + } // }}} + + function escapeBreakers (text) // {{{ + text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]+/g, function(c) uneval(c)); // }}} + + function getSidebarWindow () + document.getElementById('sidebar')._contentWindow; + + let appendTweet = (function () { // {{{ + function messageToXML (t) { + XML.prettyPrinting = true; + XML.ignoreWhitespace = true; + let xml; + let sbWidth = getSidebarWindow().document.width; + xml = + <richlistitem + class={[className('tweet-panel'), className('tweet-' + t.type)].join(' ')} + style={[ + "font-size: " + px(Config.fontSize - (t.text.length > 70 ? 2 : 0)), + "width: " + px(sbWidth - 100) + ' !important' + ].join(';')} + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <hbox> + <vbox> + <image src={t.img} height="48" width="48" /> + </vbox> + <vbox style={"width: " + (sbWidth - 48 - 35) + "px !important"}> + <hbox> + <label style="font-weight: bold">{escapeBreakers(t.name)}</label> + <spacer flex="1"/> + <label>{t.sub || ''}</label> + </hbox> + <description width="100%">{escapeBreakers(t.text)}</description> + </vbox> + </hbox> + </richlistitem>; + return xml; + } + + function xmlToDom(xml, xmlns) { + XML.prettyPrinting = true; + XML.ignoreWhitespace = true; + var doc = (new DOMParser).parseFromString( + '<root xmlns="' + xmlns + '">' + xml.toXMLString() + "</root>", + "application/xml"); + var imported = document.importNode(doc.documentElement, true); + var range = document.createRange(); + range.selectNodeContents(imported); + var fragment = range.extractContents(); + range.detach(); + return fragment.childNodes.length > 1 ? fragment : fragment.firstChild; + } + + let latest = {}; + let latestNode = null; + + function append (t, tab, streamName) { + tab = tab || 'home'; + + let now = JSON.stringify({name: t.name, text: t.text, tab: tab}); + if (latest === now) { + if (latestNode) + latestNode.setAttribute( + 'class', + latestNode.getAttributeNode('class') + className('tweet-' + t.type) + ); + return; + } + latest = now; + + let cntr = getSidebarWindow().document.getElementById('tw-anekos-sb-' + tab + '-list'); + let dom = xmlToDom(messageToXML(t)); + let repDom = dom.cloneNode(true); + let len = cntr.itemCount; + cntr.appendChild(repDom); + latestNode = repDom; + + cntr.scrollToIndex(len - 1); + if (len > Config.listMax) + cntr.removeChild(cntr.firstChild); + } + + return append; + })(); // }}} + + function objectToString (obj, head) { // {{{ + if (!head) + head = ''; + + let nextHead = head + ' '; + + let result = ''; + for (let [n, v] in Iterator(obj)) { + let vstr = + (v && typeof v === 'object') ? objectToString(v, nextHead) + : (v || '').toString(); + result += head + n + ':\n' + vstr.split(/\n/).map(function(s) nextHead + s).join('\n') + '\n'; + } + + return result.trim(); + } // }}} + + function onMsg (real, msg, raw, streamName) { // {{{ + if (real) { + Tweets.unshift(msg); + if (Tweets.length > Config.listMax) + Tweets.splice(Config.listMax); + } + + let screenName = Config.screenName; + let my = (msg.retweeted_status && msg.retweeted_status.user.screen_name === screenName) + || + (msg.target_object && msg.event && ( + (msg.event === 'favorite' && msg.target_object.user.screen_name == screenName) + || + (msg.event === 'list_member_added' && msg.target.screen_name == screenName) + )) + || + (msg.user && msg.text && Config.myNames.test(msg.text)) + || + (msg.user && msg.text && msg.in_reply_to_screen_name == screenName) + || + (msg.direct_message); + + // Fav test + try { + //liberator.log(JSON.stringify(msg, null, 2)); + if (msg.event && msg.event === 'favorite' && msg.source && msg.source.screen_name === screenName) { + let t = { + name: '>' + msg.target_object.user.screen_name + '<', + img: msg.target_object.user.profile_image_url, + text: msg.target_object.text, + type: 'favorite' + }; + appendTweet(t, 'home', streamName); + appendTweet(t, 'debug', streamName); + Config.sound.debug.play(); + } + } catch (e) { + liberator.log(e); + } + + // Ignore not JP + if (!my && streamName === 'filter' && msg.text && Config.jpOnly && !JP.test(msg.text)) { + return; + } + + if (msg.text && msg.user && msg.user && msg.user.screen_name === screenName) + my = false; + + let t, dummy; + + if (msg.direct_message) { + t = { + name: msg.direct_message.sender.screen_name, + img: msg.direct_message.sender.profile_image_url, + text: msg.direct_message.text, + sub: 'DM', + type: 'DM' + }; + } else if (msg.retweeted_status) { + t = { + name: my ? msg.user.screen_name : msg.retweeted_status.user.screen_name, + img: my ? msg.user.profile_image_url : msg.retweeted_status.user.profile_image_url, + text: msg.retweeted_status.text, + sub: '\u21BB ' + msg.user.screen_name, + type: 'retweet' + }; + dummy = true; + } else if (my && msg.target && msg.event) { + if (msg.event === 'favorite' && msg.target_object && !msg.target_object.retweeted_status) { + t = { + name: msg.source.screen_name, + img: msg.source.profile_image_url, + text: msg.target_object.text, + type: 'favorite', + sub: 'fav' + }; + dummy = true; + } else if (msg.event === 'list_member_added' && msg.target) { + // 結構漏れがある? + t = { + name: msg.source.screen_name, + img: msg.source.profile_image_url, + text: + '\u3042\u306A\u305F\u3092\u30EA\u30B9\u30C8\u300C' + + msg.target_object.name + + '\u300D\u306B\u8FFD\u52A0\u3057\u307E\u3057\u305F\u3002\n' + + 'http://twitter.com' + msg.target_object.uri, + type: 'list-member-added', + sub: 'listed' + }; + dummy = true; + } + } else if (msg.event === 'follow' && msg.target && msg.source) { + t = { + name: msg.source.screen_name, + img: msg.source.profile_image_url, + text: 'follow ' + msg.target.screen_name, + type: 'follow' + }; + my = msg.target.screen_name === screenName; + dummy = true; + } else if (msg.user && msg.text && msg.in_reply_to_screen_name == screenName) { + t = { + name: msg.user.screen_name, + img: msg.user.profile_image_url, + text: msg.text, + type: 'reply' + }; + } else if (msg.user && msg.text) { + t = { + name: msg.user.screen_name, + img: msg.user.profile_image_url, + text: msg.text, + type: 'normal' + }; + } + + if (t) { + if (Config.earthquake && /\u5730\u9707/.test(t.text) && msg.text.length < 20 && msg.user && msg.user.location) { + t.text += ' [\u5730\u57DF: ' + msg.user.location + ']'; + } + + if (!t.sub && msg.created_at) { + t.sub = new Date(msg.created_at).toLocaleTimeString().replace(/:\d+$/,'');; + } + + if (real && dummy) { + if (typeof dummy != 'object') { + dummy = { + user: { + screen_name: t.name || '', + profile_image_url: t.img + }, + text: '[' + t.type + '] ' + t.text + ' - http://twitter.com/' + t.name + }; + } + plugins.twittperator.Twittperator.onMessage(dummy); + } + + if (my || !Config.vanish.test([t.name, t.text, t.sub].join(' '))) { + if (my) { + if (real) { + let sound = Config.sound[t.type] || Config.sound.fanfare; + sound.play(); + } + t.type += '-my'; + } else { + if (t.type === 'normal' && Config.keyword.test(t.text)) + t.type = 'keyword'; + } + + if (streamName === 'filter') { + if (!msg.retweeted_status) { + t.type = 'filter'; + appendTweet(t, 'home', streamName); + appendTweet(t, 'filter', streamName); + let (s = Config.sound.filter) (s && s.play()); + } + } else if (/^(keyword)$/.test(t.type)) { + appendTweet(t, 'home', streamName); + appendTweet(t, t.type, streamName); + } else if (my) { + appendTweet(t, 'home', streamName); + appendTweet(t, 'my', streamName); + } else { + appendTweet(t, 'home', streamName); + } + } + } + + if (real) { + let s = + '----------------------------------------\n' + + objectToString(msg).replace(/\x0D\x0A|\x0D|\x0A/g, '\n'); + if (Config.logFile) + Config.logFile.write(s, '>>'); + if (my && Config.myLogFile) + Config.myLogFile.write(s, '>>'); + } + } // }}} + + function makeOnMsg (real, streamName) // {{{ + function (msg, raw) + onMsg(real, msg, raw, streamName); // }}} + + function addCommands () { // {{{ + commands.addUserCommand( + ['tws[idebar'], + 'nosidebar commands', + function (args) { + }, + { + subCommands: [ + new Command( + ['v[anish]'], + 'Vanish matched tweets', + function (args) { + Config.vanish = new RegExp(args.literalArg, 'i'); + }, + { + literal: 0, + completer: function (context, args) { + context.completions = [ + [util.escapeRegex(Config.vanish.source), ''] + ]; + } + } + ), + new Command( + ['k[eyword]'], + 'Show matched tweets in keyword tab', + function (args) { + Config.keyword = new RegExp(args.literalArg, 'i'); + }, + { + literal: 0, + completer: function (context, args) { + context.completions = [ + [util.escapeRegex(Config.keyword.source), ''] + ]; + } + } + ), + new Command( + ['j[ponly]'], + 'Show only Japanese Tweet', + function (args) { + Config.jpOnly = /yes/i.test(args.literalArg); + }, + { + literal: 0, + completer: function (context, args) { + context.completions = [ + ['yes', 'yes'], + ['no', 'no'] + ]; + } + } + ), + new Command( + ['t[ab]'], + 'select tab', + function (args) { + let tabbox = getSidebarWindow().document.getElementById('tw-anekos-sb-tabbox'); + let index = parseInt(args.literalArg, 10); + tabbox.selectedIndex = index; + }, + { + literal: 0, + completer: function (context, args) { + let tabs = getSidebarWindow().document.getElementById('tw-anekos-sb-tabbox').querySelectorAll('tab'); + context.completions = [ + [i + ': ' + tab.getAttribute('label'), tab.getAttribute('label')] + for ([i, tab] in Iterator(Array.slice(tabs))) + ]; + } + } + ) + ] + }, + true + ); + } // }}} + + /********************************************************************************* + * Install + *********************************************************************************/ + + let Store = storage.newMap("twittperator-anekos-sb", {store: true}); + let started = false; + let readyToStart = false; + + let Tweets = __context__.Tweets; + if (!Tweets) + Tweets = __context__.Tweets = Store.get("history", []); + + let added = {}; + + function start () { // {{{ + if (readyToStart) + return; + if (started) + stop(); + + readyToStart = true; + started = true; + setTimeout( + function () { + readyToStart = false; + Tweets.reverse().forEach(makeOnMsg(false)); + plugins.twittperator.ChirpUserStream.addListener(added.chirp = makeOnMsg(true, 'chirp')); + plugins.twittperator.TrackingStream.addListener(added.filter = makeOnMsg(true, 'filter')); + }, + 1000 + ); + } // }}} + + function stop () { // {{{ + if (!started) + return liberator.echoerr('TWAnekoSB has not been started!'); + + plugins.twittperator.ChirpUserStream.removeListener(added.chirp); + plugins.twittperator.TrackingStream.removeListener(added.filter); + Store.set("history", Tweets); + } // }}} + + function makeAudio (path, volume) { // {{{ + let audio = new Audio(path); + // XXX 効いてない + if (volume) + audio.volume = volume; + return audio; + } // }}} + + __context__.onUnload = function() { stop(); }; + + addCommands(); + + return {start: start, stop: stop}; + +})(); + +try { + ANekoSB.start(); +} catch (e) { + window.alert(e); +} diff --git a/twittperator/twsidebar/chrome.manifest b/twittperator/twsidebar/chrome.manifest new file mode 100644 index 0000000..09edd9a --- /dev/null +++ b/twittperator/twsidebar/chrome.manifest @@ -0,0 +1,4 @@ +content twsidebar chrome/content/ +locale twsidebar en-US chrome/locale/en-US/ +skin twsidebar classic/1.0 chrome/skin/ +overlay chrome://browser/content/browser.xul chrome://twsidebar/content/overlay.xul diff --git a/twittperator/twsidebar/chrome/content/overlay.xul b/twittperator/twsidebar/chrome/content/overlay.xul new file mode 100644 index 0000000..59e7a70 --- /dev/null +++ b/twittperator/twsidebar/chrome/content/overlay.xul @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<overlay id="twitter-sidebar-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <menupopup id="viewSidebarMenu"> + <menuitem key="key_openEmptySidebar" observes="viewTwitteerSidebar" /> + </menupopup> + + <keyset id="mainKeyset"> + <key id="key_openEmptySidebar" command="viewTwitteerSidebar" + key="E" + modifiers="shift accel" /> + </keyset> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="viewTwitteerSidebar" + label="Twitter Sidebar" + autoCheck="false" + type="checkbox" + group="sidebar" + sidebarurl="chrome://twsidebar/content/twsidebar.xul" + sidebartitle="Twitter Sidebar" + oncommand="toggleSidebar('viewTwitteerSidebar');" /> + </broadcasterset> +</overlay> diff --git a/twittperator/twsidebar/chrome/content/twsidebar.css b/twittperator/twsidebar/chrome/content/twsidebar.css new file mode 100644 index 0000000..b835c3d --- /dev/null +++ b/twittperator/twsidebar/chrome/content/twsidebar.css @@ -0,0 +1,50 @@ +#tw-anekos-sb-tab-panels { + background-color: transparent !important; + border: none !important; + padding: 0 !important; +} + +.tw-anekos-sb-plugin-tweet-panel { + background-color: floralwhite !important; + font-size: %Config.fontSize%px; + padding-top: 2px; + padding-left: 2px; +} + +.tw-anekos-sb-plugin-tweet-DM-my { + background-color: darkred !important; + color: white !important; +} + +.tw-anekos-sb-plugin-tweet-retweet-my { + background-color: aqua !important; +} + +.tw-anekos-sb-plugin-tweet-favorite-my { + background-color: pink !important; +} + +.tw-anekos-sb-plugin-tweet-reply-my { + background-color: yellow !important; +} + +.tw-anekos-sb-plugin-tweet-normal-my { + background-color: yellow !important; +} + +.tw-anekos-sb-plugin-tweet-keyword { + background-color: lightgreen !important; +} + +.tw-anekos-sb-plugin-tweet-filter { + background-color: #66cdaa !important; +} + +.tw-anekos-sb-plugin-tweet-list-member-added-my { + background-color: lightskyblue !important; +} + +.tw-anekos-sb-plugin-tweet-follow, +.tw-anekos-sb-plugin-tweet-follow-my { + background-color: lightsalmon !important; +} diff --git a/twittperator/twsidebar/chrome/content/twsidebar.xul b/twittperator/twsidebar/chrome/content/twsidebar.xul new file mode 100644 index 0000000..69ab965 --- /dev/null +++ b/twittperator/twsidebar/chrome/content/twsidebar.xul @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?> +<?xml-stylesheet href="chrome://twsidebar/content/twsidebar.css" type="text/css"?> +<page id="tw-sidebar-page" title="Twitter Sidebar" + onload="window.parent.liberator.modules.TWAnekoSB.start()" + onunload="window.parent.liberator.modules.TWAnekoSB.stop()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" > + <vbox flex="1"> + <tabbox id="tw-anekos-sb-tabbox" flex="1"> + <tabs> + <tab label="Home"/> + <tab label="My"/> + <tab label="Keyword"/> + <tab label="Filter"/> + <tab label="Debug"/> + </tabs> + <tabpanels flex="1" id="tw-anekos-sb-tab-panels" style="background: transparent;"> + <tabpanel flex="1"> + <richlistbox id="tw-anekos-sb-home-list" contextmenu="contentAreaContextMenu" flex="1"/> + </tabpanel> + <tabpanel flex="1"> + <richlistbox id="tw-anekos-sb-my-list" contextmenu="contentAreaContextMenu" flex="1"/> + </tabpanel> + <tabpanel flex="1"> + <richlistbox id="tw-anekos-sb-keyword-list" contextmenu="contentAreaContextMenu" flex="1"/> + </tabpanel> + <tabpanel flex="1"> + <richlistbox id="tw-anekos-sb-filter-list" contextmenu="contentAreaContextMenu" flex="1"/> + </tabpanel> + <tabpanel flex="1"> + <richlistbox id="tw-anekos-sb-debug-list" contextmenu="contentAreaContextMenu" flex="1"/> + </tabpanel> + </tabpanels> + </tabbox> + </vbox> +</page> diff --git a/twittperator/twsidebar/chrome/locale/en-US/nosidebar.dtd b/twittperator/twsidebar/chrome/locale/en-US/nosidebar.dtd new file mode 100644 index 0000000..18f4ba8 --- /dev/null +++ b/twittperator/twsidebar/chrome/locale/en-US/nosidebar.dtd @@ -0,0 +1,3 @@ +<!ENTITY emptysidebar.title "EmptySidebar"> +<!ENTITY openEmptySidebar.commandkey "E"> +<!ENTITY openEmptySidebar.modifierskey "shift accel">
\ No newline at end of file diff --git a/twittperator/twsidebar/install.rdf b/twittperator/twsidebar/install.rdf new file mode 100644 index 0000000..f56b72a --- /dev/null +++ b/twittperator/twsidebar/install.rdf @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>twsidebar@snca.net</em:id> + <em:name>Twitter Sidebar</em:name> + <em:version>1.0</em:version> + <em:creator>anekos</em:creator> + <em:description>Twitter Sidebar for Twittperator Plugin</em:description> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- firefox --> + <em:minVersion>3.6</em:minVersion> + <em:maxVersion>10.0a1</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> |