您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
See website
// ==UserScript== // @name BetterFetLife // @namespace com.fetlife.better // @include https://fetlife.com/* // @version 1.4 // @grant none // @description See website // ==/UserScript== $(document).ready(function(){ // add custom css $('head').append('<style type="text/css">' + bfl_css + '</style>'); // === USER POPUP === // // create user popup $('body').append(' ' + ' <div id="bfl-user"> ' + ' <a class="avatar-wrap"> ' + ' <div class="avatar"></div> ' + ' </a> ' + ' <a class="name"></a> ' + ' <span class="status"></span> ' + ' <span class="location"></span> ' + ' <span class="photos"></span> ' + ' </div> ' ); // show a user popup on hover $(document).hoverIntent({ selector: 'a[href^="/users/"], a[href^="https://fetlife.com/users/"]', over: function(){ var linkEl = this; var href = $(linkEl).attr('href'); href = href.replace('https://fetlife.com', ''); // prevent non-user links if( href.split('/').length != 3 ) return; // prevent self-links if( $(linkEl).closest('#bfl-user').length > 0 ) return; // prevent closing clearTimeout(hideUserPopupTimeout); // reset the popup hideUserPopup(); // show the popup $('#bfl-user') .addClass('loading') .css({ top: $(linkEl).offset().top + $(linkEl).height() + 8, left: $(linkEl).offset().left }) .show(); $.ajax({ url: href, dataType: "html", // cache is OK cache: true, // prevent 503, fetlife don't liking ajax calls beforeSend: function(xhr) { xhr.setRequestHeader( 'X-Requested-With', { toString: function() { return ''; } } ); }, success: function(userDOM){ var userAvatarEl = $(userDOM).find('#main_content a img'); // avatar href $('#bfl-user') .attr('avatar-href', $(userAvatarEl).attr('src')); // avatar $('#bfl-user .avatar-wrap') .attr('href', href) $('#bfl-user .avatar') .css('background-image', 'url(' + $(userAvatarEl).attr('src') + ')') // name $('#bfl-user .name') .attr('href', href) .html( $(userAvatarEl).attr('alt') ); // status (age+gender+orientation) $('#bfl-user .status') .html( $(userDOM).find('#profile h2 .small').html() ); // location $('#bfl-user .location') .html( $(userDOM).find('#profile h2.bottom + p').html() ); window.userDOM = userDOM; // photos var photos = $(userDOM).find('#profile .container a[href^="/users/"][href*="/pictures"]'); photos = photos.filter(function(){ return $(this).find('img').length > 0; }) photos = photos.slice(0,5); $('#bfl-user .photos') .html('') .append( photos ); // friends status // remove the link first $(userDOM).find('.friends_badge').find('a').remove() $('#bfl-user .friends_status') .html( $(userDOM).find('.friends_badge').text() ); $('#bfl-user') .removeClass('loading') } }); }, out: function(e){ var linkEl = this; var href = $(linkEl).attr('href'); // prevent non-user links if( href.split('/').length != 3 ) return; clearTimeout(hideUserPopupTimeout); hideUserPopupTimeout = setTimeout(hideUserPopup, hideUserPopupDelay); } }); $(document).on('mouseover', '#bfl-user', function(e){ clearTimeout(hideUserPopupTimeout); }); $(document).on('mouseleave', '#bfl-user', function(e){ clearTimeout(hideUserPopupTimeout); hideUserPopupTimeout = setTimeout(hideUserPopup, hideUserPopupDelay); }); var hideUserPopupTimeout = setTimeout('', 0); var hideUserPopupDelay = 500; function hideUserPopup() { $('#bfl-user .avatar').attr('style', ''); $('#bfl-user .name').html('').attr('href', ''); $('#bfl-user .status').html(''); $('#bfl-user .location').html(''); $('#bfl-user .friends_badge').html(''); $('#bfl-user .photos').html(''); $('#bfl-user').removeClass('loading'); $('#bfl-user').hide(); } // === IMAGE POPUP === // // create image popup $('body').append(' ' + ' <div id="bfl-image"> ' + ' <span class="header"> ' + ' <span class="title"></span> ' + ' <span class="like-wrap"> ' + ' <span class="like-count"></span> ' + ' <span class="like picto">k</span> ' + ' </span> ' + ' </span> ' + ' <a class="image-wrap"> ' + ' <img class="image" /> ' + ' </a> ' + ' </div> ' ); $(document).hoverIntent({ selector: 'a[href^="/users/"][href*="/pictures"], a[href^="https://fetlife.com/users/"][href*="/pictures"]', over: function(){ var linkEl = this; var href = $(this).attr('href'); href = href.replace('https://fetlife.com', ''); // prevent non-image links if( href.split('/').length != 5 ) return; // prevent self-links if( $(linkEl).closest('#bfl-image').length > 0 ) return; // prevent 'next image' if( $(linkEl).children('.fake_img').length > 0 ) return; // reset the popup hideImagePopup(); // show the popup var css = { top: $(linkEl).offset().top + $(linkEl).height() + 8 } if( $(linkEl).offset().left > $(window).width()/2 ) { css.right = $(window).width() - $(linkEl).offset().left - $(linkEl).width(); $('#bfl-image').addClass('alignright'); } else { css.left = $(linkEl).offset().left; } $('#bfl-image') .addClass('loading') .css(css) .show(); $.ajax({ url: href, dataType: "html", success: function(html){ var title = $(html).find('.s.i.caption').text(); var likeUrl = href.split('/'); likeUrl = likeUrl[ likeUrl.length-1 ]; likeUrl = "/pictures/" + likeUrl + "/likes" // extract the image src var src = $(html).find('style').first().html().match(/\(\'(.*?)\'\)/); src = src[0]; src = src.replace("('", ""); src = src.replace("')", ""); $('#bfl-image .title') .html(title) .attr('title', title) $('#bfl-image .like-wrap') .attr('data-href', likeUrl); $('#bfl-image .image-wrap') .attr('href', href); $('#bfl-image .image') .load(function(){ $('#bfl-image').removeClass('loading') }) .attr('src', src) // get amount of likes $.ajax({ url: likeUrl, dataType: "json", success: function(data) { $('#bfl-image .like-wrap').toggle(data.user_can_like); if( data.is_liked_by_user ) { $('#bfl-image .like-wrap').addClass('liked'); } $('#bfl-image .like-count') .html(data.total); } }); } }); }, out: function(e){ var linkEl = this; var href = $(linkEl).attr('href'); // prevent non-user links if( href.split('/').length != 5 ) return; clearTimeout(hideImagePopupTimeout); hideImagePopupTimeout = setTimeout(hideImagePopup, hideImagePopupDelay); } }); $(document).on('mouseover', '#bfl-image', function(e){ clearTimeout(hideImagePopupTimeout); clearTimeout(hideUserPopupTimeout); }); $(document).on('mouseleave', '#bfl-image', function(e){ clearTimeout(hideImagePopupTimeout); hideImagePopupTimeout = setTimeout(hideImagePopup, hideImagePopupDelay); }); var hideImagePopupTimeout = setTimeout('', 0); var hideImagePopupDelay = 500; function hideImagePopup() { $('#bfl-image .title').html('').attr('href', ''); $('#bfl-image .image').attr('src', ''); $('#bfl-image .like-wrap').attr('data-href', ''); $('#bfl-image .like-count').html(''); $('#bfl-image').removeClass('loading'); $('#bfl-image').removeClass('alignright'); $('#bfl-image').hide(); } $(document).on('click', '#bfl-image .like-wrap', function(){ var this_ = this; $.ajax({ url: $(this_).data('href') + '/toggle', type: 'post', success: function(){ if( $(this_).hasClass('liked') ) { $('#bfl-image .like-count').html( parseInt( $('#bfl-image .like-count').html()) - 1 ) } else { $('#bfl-image .like-count').html( parseInt( $('#bfl-image .like-count').html()) + 1 ) } $(this_).toggleClass('liked'); } }); return false; }); }); var bfl_css = '' + ' #bfl-user {' + ' position: absolute;' + ' z-index: 100;' + ' display: none;' + ' padding: 4px;' + ' min-width: 180px;' + ' height: 80px;' + ' padding-left: 92px;' + ' padding-right: 8px;' + ' background: #323232;' + ' border: 3px solid #171717;' + ' }' + ' #bfl-user.loading {' + ' padding-left: 84px;' + ' padding-right: 4px;' + ' min-width: 0;' + ' }' + ' #bfl-user:before,' + ' #bfl-image:before {' + ' position: absolute;' + ' z-index: 101;' + ' display: block;' + ' content: "";' + ' left: 7px;' + ' top: -8px;' + ' border: 8px solid transparent;' + ' border-bottom-color: #171717;' + ' border-top-width: 0;' + ' }' + ' #bfl-user:after,' + ' #bfl-image:after {' + ' position: absolute;' + ' z-index: 102;' + ' display: block;' + ' content: "";' + ' left: 10px;' + ' top: -5px;' + ' border: 5px solid transparent;' + ' border-bottom-color: #323232;' + ' border-top-width: 0;' + ' }' + ' #bfl-user .avatar {' + ' position: absolute;' + ' left: 4px;' + ' width: 80px;' + ' height: 80px;' + ' padding: 0px;' + ' margin-right: 8px;' + ' background-color: transparent;' + ' background-size: cover;' + ' background-position: center center;' + ' background-repeat: no-repeat;' + ' }' + ' #bfl-user.loading .avatar {' + ' background-size: auto;' + ' background-image: url(https://flassets.a.ssl.fastly.net/std/spinners/circle_big.gif);' + ' margin-right: 0;' + ' }' + ' #bfl-user .name {' + ' white-space: nowrap;' + ' }' + ' #bfl-user .status {' + ' white-space: nowrap;' + ' color: #aaa;' + ' }' + ' #bfl-user .location {' + ' display: block;' + ' font-size: 12px;' + ' white-space: nowrap;' + ' }' + ' #bfl-user .friends_status {' + ' float: right;' + ' font-size: 12px;' + ' }' + ' #bfl-user .photos {' + ' position: absolute;' + ' right: 4px;' + ' bottom: 4px;' + ' font-size: 12px;' + ' }' + ' #bfl-user .photos a {' + ' float: left;' + ' font-size: 12px;' + ' }' + ' #bfl-user .photos a img {' + ' float: left;' + ' margin: 2px;' + ' width: 25px;' + ' height: 25px;' + ' padding: 0;' + ' }' + ' #bfl-image {' + ' position: absolute;' + ' z-index: 100;' + ' display: none;' + ' background: #323232;' + ' border: 3px solid #171717;' + ' padding: 4px;' + ' }' + ' #bfl-image.alignright:before {' + ' left: auto;' + ' right: 7px;' + ' }' + ' #bfl-image.alignright:after {' + ' left: auto;' + ' right: 10px;' + ' }' + ' #bfl-image.loading {' + ' width: 80px;' + ' height: 80px;' + ' background: #323232 url(https://flassets.a.ssl.fastly.net/std/spinners/circle_big.gif) no-repeat center center;' + ' }' + ' #bfl-image .header {' + ' position: absolute;' + ' left: 0;' + ' right: 0;' + ' top: 0;' + ' background: #323232;' + ' padding: 4px;' + ' overflow: hidden;' + ' font-size: 12px;;' + ' }' + ' #bfl-image.loading .header {' + ' display: none;' + ' }' + ' #bfl-image .title {' + ' float: left;' + ' width: 80%;' + ' white-space: nowrap;' + ' overflow: hidden;' + ' text-overflow: ellipsis;' + ' }' + ' #bfl-image .like-wrap {' + ' float: right;' + ' width: 10%;' + ' white-space: nowrap;' + ' text-align: right;' + ' cursor: pointer;' + ' }' + ' #bfl-image .like-wrap:hover {' + ' color: #ffffff;' + ' }' + ' #bfl-image .like-wrap:active {' + ' color: #bbbbbb;' + ' }' + ' #bfl-image .like-wrap.liked {' + ' color: #DD0000;' + ' }' + ' #bfl-image .like-wrap.liked:hover {' + ' color: #FF0000;' + ' }' + ' #bfl-image .like-wrap.liked:active {' + ' color: #BB0000;' + ' }' + ' #bfl-image .image {' + ' display: block;' + ' padding: 0;' + ' }' + ' #bfl-image.loading .image {' + ' opacity: 0;' + ' }' + /*! * hoverIntent v1.8.0 // 2014.06.29 // jQuery v1.9.1+ * http://cherne.net/brian/resources/jquery.hoverIntent.html * * You may use hoverIntent under the terms of the MIT license. Basically that * means you are free to use hoverIntent as long as this header is left intact. * Copyright 2007, 2014 Brian Cherne */ /* hoverIntent is similar to jQuery's built-in "hover" method except that * instead of firing the handlerIn function immediately, hoverIntent checks * to see if the user's mouse has slowed down (beneath the sensitivity * threshold) before firing the event. The handlerOut function is only * called after a matching handlerIn. * * // basic usage ... just like .hover() * .hoverIntent( handlerIn, handlerOut ) * .hoverIntent( handlerInOut ) * * // basic usage ... with event delegation! * .hoverIntent( handlerIn, handlerOut, selector ) * .hoverIntent( handlerInOut, selector ) * * // using a basic configuration object * .hoverIntent( config ) * * @param handlerIn function OR configuration object * @param handlerOut function OR selector for delegation OR undefined * @param selector selector OR undefined * @author Brian Cherne <brian(at)cherne(dot)net> */ (function($) { $.fn.hoverIntent = function(handlerIn,handlerOut,selector) { // default configuration values var cfg = { interval: 100, sensitivity: 6, timeout: 0 }; if ( typeof handlerIn === "object" ) { cfg = $.extend(cfg, handlerIn ); } else if ($.isFunction(handlerOut)) { cfg = $.extend(cfg, { over: handlerIn, out: handlerOut, selector: selector } ); } else { cfg = $.extend(cfg, { over: handlerIn, out: handlerIn, selector: handlerOut } ); } // instantiate variables // cX, cY = current X and Y position of mouse, updated by mousemove event // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval var cX, cY, pX, pY; // A private function for getting mouse position var track = function(ev) { cX = ev.pageX; cY = ev.pageY; }; // A private function for comparing current and previous mouse position var compare = function(ev,ob) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); // compare mouse positions to see if they've crossed the threshold if ( Math.sqrt( (pX-cX)*(pX-cX) + (pY-cY)*(pY-cY) ) < cfg.sensitivity ) { $(ob).off("mousemove.hoverIntent",track); // set hoverIntent state to true (so mouseOut can be called) ob.hoverIntent_s = true; return cfg.over.apply(ob,[ev]); } else { // set previous coordinates for next time pX = cX; pY = cY; // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs) ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval ); } }; // A private function for delaying the mouseOut function var delay = function(ev,ob) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); ob.hoverIntent_s = false; return cfg.out.apply(ob,[ev]); }; // A private function for handling mouse 'hovering' var handleHover = function(e) { // copy objects to be passed into t (required for event object to be passed in IE) var ev = $.extend({},e); var ob = this; // cancel hoverIntent timer if it exists if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); } // if e.type === "mouseenter" if (e.type === "mouseenter") { // set "previous" X and Y position based on initial entry point pX = ev.pageX; pY = ev.pageY; // update "current" X and Y position based on mousemove $(ob).on("mousemove.hoverIntent",track); // start polling interval (self-calling timeout) to compare mouse coordinates over time if (!ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );} // else e.type == "mouseleave" } else { // unbind expensive mousemove event $(ob).off("mousemove.hoverIntent",track); // if hoverIntent state is true, then call the mouseOut function after the specified delay if (ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );} } }; // listen for mouseenter and mouseleave return this.on({'mouseenter.hoverIntent':handleHover,'mouseleave.hoverIntent':handleHover}, cfg.selector); }; })(jQuery);