- // ==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);