// ==UserScript==
// @name BetterFetLife
// @namespace com.fetlife.better
// @include https://fetlife.com/*
// @version 1.3
// @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
$('a[href^="/users/"], a[href^="https://fetlife.com/users/"').hoverIntent(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')
}
});
}, 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);
});
$('#bfl-user').live('mouseover', function(e){
clearTimeout(hideUserPopupTimeout);
});
$('#bfl-user').live('mouseleave', 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> '
);
$('a[href^="/users/"][href*="/pictures"], a[href^="https://fetlife.com/users/"][href*="/pictures"]').hoverIntent(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);
}
});
}
});
}, 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);
});
$('#bfl-image').live('mouseover', function(e){
clearTimeout(hideImagePopupTimeout);
clearTimeout(hideUserPopupTimeout);
});
$('#bfl-image').live('mouseleave', 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();
}
$('#bfl-image .like-wrap').live('click', 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" function except that
* instead of firing the onMouseOver event immediately, hoverIntent checks
* to see if the user's mouse has slowed down (beneath the sensitivity
* threshold) before firing the onMouseOver event.
*
* hoverIntent r6 // 2011.02.26 // jQuery 1.5.1+
* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
*
* hoverIntent is currently available for use in all personal or commercial
* projects under both MIT and GPL licenses. This means that you can choose
* the license that best suits your project, and use it accordingly.
*
* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
* $("ul li").hoverIntent( showNav , hideNav );
*
* // advanced usage receives configuration object only
* $("ul li").hoverIntent({
* sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
* interval: 100, // number = milliseconds of polling interval
* over: showNav, // function = onMouseOver callback (required)
* timeout: 0, // number = milliseconds delay before onMouseOut function call
* out: hideNav // function = onMouseOut callback (required)
* });
*
* @param f onMouseOver function || An object with configuration options
* @param g onMouseOut function || Nothing (use configuration options object)
* @author Brian Cherne brian(at)cherne(dot)net
*/
(function($) {
$.fn.hoverIntent = function(f,g) {
// default configuration options
var cfg = {
sensitivity: 7,
interval: 100,
timeout: 0
};
// override configuration options with user supplied object
cfg = $.extend(cfg, g ? { over: f, out: g } : f );
// 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.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
$(ob).unbind("mousemove",track);
// set hoverIntent state to true (so mouseOut can be called)
ob.hoverIntent_s = 1;
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 = 0;
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 = jQuery.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).bind("mousemove",track);
// start polling interval (self-calling timeout) to compare mouse coordinates over time
if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
// else e.type == "mouseleave"
} else {
// unbind expensive mousemove event
$(ob).unbind("mousemove",track);
// if hoverIntent state is true, then call the mouseOut function after the specified delay
if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
}
};
// bind the function to the two event listeners
//return this.bind('mouseenter',handleHover).bind('mouseleave',handleHover);
return this.live('hover', handleHover);
};
})(jQuery);