您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hover over FetLife user avatar pictures to see their recent activity, vitals, group subscriptions, and more. Quickly discern whether they're worth talking back to or not.
/** * * This is a Greasemonkey script and must be run using a Greasemonkey-compatible browser. * * @author maymay <[email protected]> */ // ==UserScript== // @name FetLife Spyscope // @version 0.1.1 // @namespace http://maybemaimed.com/playground/fetlife-spyscope/ // @description Hover over FetLife user avatar pictures to see their recent activity, vitals, group subscriptions, and more. Quickly discern whether they're worth talking back to or not. // @include https://fetlife.com/* // @exclude https://fetlife.com/adgear/* // @exclude https://fetlife.com/chat/* // @exclude https://fetlife.com/im_sessions* // @exclude https://fetlife.com/polling/* // @exclude https://fetlife.com/users/*/friends // @grant GM_xmlhttpRequest // @grant GM_addStyle // ==/UserScript== FL_SPYSCOPE = {}; FL_SPYSCOPE.CONFIG = { 'debug': false, // switch to true to debug. }; // Utility debugging function. FL_SPYSCOPE.log = function (msg) { if (!FL_SPYSCOPE.CONFIG.debug) { return; } GM_log('FETLIFE SPYSCOPE: ' + msg); }; // Initializations. var uw = (unsafeWindow) ? unsafeWindow : window ; // Help with Chrome compatibility? GM_addStyle('\ .fl-spyscope {\ border: 1px solid gray;\ max-width: 500px;\ }\ .group_mods .fl-spyscope {\ float: left;\ }\ .post .fl-spyscope,\ .pictures .fl-spyscope,\ .friends .fl-spyscope {\ position: absolute;\ background: black;\ }\ .post .last_comment .fl-spyscope {\ position: static;\ }\ /* Events page and friends lists. */\ .pictures > li,\ .friends > li { position: relative; }\ .pictures .fl-spyscope,\ .friends .fl-spyscope { z-index: 100; }\ .pictures .fl-spyscope { top: 60px; }\ .friends .fl-spyscope { top: 40px; }\ .pictures .fl-spyscope li,\ #profile .friends .fl-spyscope li { width: 300px; }\ .pictures .fl-spyscope a { float: none; }\ #profile .friends .fl-spyscope a { display: inline; }\ .friends .fl-spyscope img { float: none; }\ '); FL_SPYSCOPE.users_cache = {}; FL_SPYSCOPE.init = function () { FL_SPYSCOPE.main(); }; window.addEventListener('DOMContentLoaded', FL_SPYSCOPE.init); /** * Given a user ID, fetch and store the complete FetLife profile HTML for this user. * * @see https://userscripts.org/scripts/review/146293#function.FL_SPYSCOPE.getUserProfile */ FL_SPYSCOPE.fetchUserProfile = function (id) { if (FL_SPYSCOPE.users_cache[id].profile_html) { return; // we've already got a cache, so don't do this again. } FL_SPYSCOPE.users_cache[id] = {}; GM_xmlhttpRequest({ 'method': 'GET', 'url': 'https://fetlife.com/users/' + id.toString(), 'onload': function (response) { // Get profile HTML. var html = response.responseText; // Store it in a local cache? FL_SPYSCOPE.users_cache[id].profile_html = html; // Parse the returned profile HTML and save relevant info. var parser = new DOMParser(); var doc = parser.parseFromString(html, 'text/html'); // NOTE: We collect the nickname in the FL_SPYSCOPE.main() function, since we can. FL_SPYSCOPE.users_cache[id].nickname = doc.querySelector('.bottom').childNodes[0].nodeValue; FL_SPYSCOPE.users_cache[id].sex = FL_SPYSCOPE.getSex(doc.querySelector('.bottom')); FL_SPYSCOPE.users_cache[id].age = FL_SPYSCOPE.getAge(doc.querySelector('.bottom')); FL_SPYSCOPE.users_cache[id].role = FL_SPYSCOPE.getRole(doc.querySelector('.bottom')); FL_SPYSCOPE.users_cache[id].loc_str = doc.querySelector('.bottom + p').innerHTML; FL_SPYSCOPE.users_cache[id].activity = doc.querySelector('#mini_feed'); } }); }; /** * Various user info parsing functions. * * @see https://userscripts.org/scripts/review/146293#function.FL_SPYSCOPE.getSex */ FL_SPYSCOPE.getSex = function (el) { x = el.querySelector('.quiet').innerHTML; sex = x.match(/^\d\d(\S*)/); return sex[1]; }; FL_SPYSCOPE.getAge = function (el) { x = el.querySelector('.quiet').innerHTML; age = x.match(/^\d\d/); return parseInt(age); }; FL_SPYSCOPE.getRole = function (el) { x = el.querySelector('.quiet').innerHTML; role = x.match(/ ?(\S+)?$/); return role[1]; }; FL_SPYSCOPE.createScope = function (id) { var div = document.createElement('div'); div.setAttribute('class', 'fl-spyscope'); var ul = document.createElement('ul'); var li = document.createElement('li'); var html_str = ''; // Fill list items appropriately. html_str += '<b><font color="red">'; html_str += FL_SPYSCOPE.users_cache[id].nickname; html_str += '</font> '; html_str += FL_SPYSCOPE.users_cache[id].age; html_str += FL_SPYSCOPE.users_cache[id].sex; html_str += ' '; html_str += FL_SPYSCOPE.users_cache[id].role + '</b><br />'; html_str += '(' + FL_SPYSCOPE.users_cache[id].loc_str + ')'; li.innerHTML = html_str; ul.appendChild(li); div.appendChild(ul); // Show last three items from "Latest activity" mini feed. //var acts = FL_SPYSCOPE.users_cache[id].activity.children; //div.innerHTML += '<ul><li>' + acts[0].innerHTML + '</li><li>' + acts[1].innerHTML + '</li><li>' + acts[2].innerHTML + '</li></ul>'; return div; }; /** * Handles spyscope rollovers. */ FL_SPYSCOPE.show = function (e) { scope = FL_SPYSCOPE.createScope(parseInt(e.currentTarget.href.match(/\d+$/))); e.currentTarget.parentNode.appendChild(scope); }; FL_SPYSCOPE.hide = function (e) { var scope = e.currentTarget.parentNode.lastChild; scope.parentNode.removeChild(scope); }; // This is the main() function, executed on page load. FL_SPYSCOPE.main = function () { // Find all FetLife users on this page that aren't the current (logged-in) user. var user_links = document.querySelectorAll('a[href^="/users/"]:not([href^="/users/' + uw.FetLife.currentUser.id + '"])'); // For each user, for (var i = 0; i < user_links.length; i++) { // Collect its user ID number. var id = parseInt(user_links[i].href.match(/(\d+)\/?$/)); FL_SPYSCOPE.users_cache[id] = {}; if (null !== id) { // Gather profile data for this user. // Get nickname. var n; if (user_links[i].children.length) { // This is an avatar link, not a text link. n = user_links[i].childNodes[0].alt; user_links[i].childNodes[0].title = ""; } else { // This is a text link. Easy. n = user_links[i].innerHTML; } FL_SPYSCOPE.users_cache[id].nickname = n; // Collect age/sex/role/location. // TODO: Loading them all on page load really slows things down. // Can we optimize so that we fetch on rollover rather than load? FL_SPYSCOPE.fetchUserProfile(id); } // Attach the spyscope show/hide event handler. user_links[i].addEventListener('mouseover', FL_SPYSCOPE.show); user_links[i].addEventListener('mouseout', FL_SPYSCOPE.hide); } }; // The following is required for Chrome compatibility, as we need "text/html" parsing. /* * DOMParser HTML extension * 2012-09-04 * * By Eli Grey, http://eligrey.com * Public domain. * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. */ /*! @source https://gist.github.com/1129031 */ /*global document, DOMParser*/ (function(DOMParser) { "use strict"; var DOMParser_proto = DOMParser.prototype , real_parseFromString = DOMParser_proto.parseFromString ; // Firefox/Opera/IE throw errors on unsupported types try { // WebKit returns null on unsupported types if ((new DOMParser).parseFromString("", "text/html")) { // text/html parsing is natively supported return; } } catch (ex) {} DOMParser_proto.parseFromString = function(markup, type) { if (/^\s*text\/html\s*(?:;|$)/i.test(type)) { var doc = document.implementation.createHTMLDocument("") ; doc.body.innerHTML = markup; return doc; } else { return real_parseFromString.apply(this, arguments); } }; }(DOMParser));