BetterFetLife

See website

  1. // ==UserScript==
  2. // @name BetterFetLife
  3. // @namespace com.fetlife.better
  4. // @include https://fetlife.com/*
  5. // @version 1.4
  6. // @grant none
  7. // @description See website
  8. // ==/UserScript==
  9.  
  10. $(document).ready(function(){
  11. // add custom css
  12. $('head').append('<style type="text/css">' + bfl_css + '</style>');
  13. // === USER POPUP === //
  14. // create user popup
  15. $('body').append(' ' +
  16. ' <div id="bfl-user"> ' +
  17. ' <a class="avatar-wrap"> ' +
  18. ' <div class="avatar"></div> ' +
  19. ' </a> ' +
  20. ' <a class="name"></a> ' +
  21. ' <span class="status"></span> ' +
  22. ' <span class="location"></span> ' +
  23. ' <span class="photos"></span> ' +
  24. ' </div> '
  25. );
  26. // show a user popup on hover
  27. $(document).hoverIntent({
  28. selector: 'a[href^="/users/"], a[href^="https://fetlife.com/users/"]',
  29. over: function(){
  30. var linkEl = this;
  31. var href = $(linkEl).attr('href');
  32. href = href.replace('https://fetlife.com', '');
  33. // prevent non-user links
  34. if( href.split('/').length != 3 ) return;
  35. // prevent self-links
  36. if( $(linkEl).closest('#bfl-user').length > 0 ) return;
  37. // prevent closing
  38. clearTimeout(hideUserPopupTimeout);
  39. // reset the popup
  40. hideUserPopup();
  41. // show the popup
  42. $('#bfl-user')
  43. .addClass('loading')
  44. .css({
  45. top: $(linkEl).offset().top + $(linkEl).height() + 8,
  46. left: $(linkEl).offset().left
  47. })
  48. .show();
  49. $.ajax({
  50. url: href,
  51. dataType: "html",
  52. // cache is OK
  53. cache: true,
  54. // prevent 503, fetlife don't liking ajax calls
  55. beforeSend: function(xhr) {
  56. xhr.setRequestHeader(
  57. 'X-Requested-With',
  58. {
  59. toString: function() { return ''; }
  60. }
  61. );
  62. },
  63. success: function(userDOM){
  64. var userAvatarEl = $(userDOM).find('#main_content a img');
  65. // avatar href
  66. $('#bfl-user')
  67. .attr('avatar-href', $(userAvatarEl).attr('src'));
  68. // avatar
  69. $('#bfl-user .avatar-wrap')
  70. .attr('href', href)
  71. $('#bfl-user .avatar')
  72. .css('background-image', 'url(' + $(userAvatarEl).attr('src') + ')')
  73. // name
  74. $('#bfl-user .name')
  75. .attr('href', href)
  76. .html( $(userAvatarEl).attr('alt') );
  77. // status (age+gender+orientation)
  78. $('#bfl-user .status')
  79. .html( $(userDOM).find('#profile h2 .small').html() );
  80. // location
  81. $('#bfl-user .location')
  82. .html( $(userDOM).find('#profile h2.bottom + p').html() );
  83. window.userDOM = userDOM;
  84. // photos
  85. var photos = $(userDOM).find('#profile .container a[href^="/users/"][href*="/pictures"]');
  86. photos = photos.filter(function(){
  87. return $(this).find('img').length > 0;
  88. })
  89. photos = photos.slice(0,5);
  90. $('#bfl-user .photos')
  91. .html('')
  92. .append( photos );
  93. // friends status
  94. // remove the link first
  95. $(userDOM).find('.friends_badge').find('a').remove()
  96. $('#bfl-user .friends_status')
  97. .html( $(userDOM).find('.friends_badge').text() );
  98. $('#bfl-user')
  99. .removeClass('loading')
  100. }
  101. });
  102. },
  103. out: function(e){
  104. var linkEl = this;
  105. var href = $(linkEl).attr('href');
  106. // prevent non-user links
  107. if( href.split('/').length != 3 ) return;
  108. clearTimeout(hideUserPopupTimeout);
  109. hideUserPopupTimeout = setTimeout(hideUserPopup, hideUserPopupDelay);
  110. }
  111. });
  112. $(document).on('mouseover', '#bfl-user', function(e){
  113. clearTimeout(hideUserPopupTimeout);
  114. });
  115. $(document).on('mouseleave', '#bfl-user', function(e){
  116. clearTimeout(hideUserPopupTimeout);
  117. hideUserPopupTimeout = setTimeout(hideUserPopup, hideUserPopupDelay);
  118. });
  119.  
  120. var hideUserPopupTimeout = setTimeout('', 0);
  121. var hideUserPopupDelay = 500;
  122. function hideUserPopup() {
  123. $('#bfl-user .avatar').attr('style', '');
  124. $('#bfl-user .name').html('').attr('href', '');
  125. $('#bfl-user .status').html('');
  126. $('#bfl-user .location').html('');
  127. $('#bfl-user .friends_badge').html('');
  128. $('#bfl-user .photos').html('');
  129. $('#bfl-user').removeClass('loading');
  130. $('#bfl-user').hide();
  131. }
  132. // === IMAGE POPUP === //
  133. // create image popup
  134. $('body').append(' ' +
  135. ' <div id="bfl-image"> ' +
  136. ' <span class="header"> ' +
  137. ' <span class="title"></span> ' +
  138. ' <span class="like-wrap"> ' +
  139. ' <span class="like-count"></span> ' +
  140. ' <span class="like picto">k</span> ' +
  141. ' </span> ' +
  142. ' </span> ' +
  143. ' <a class="image-wrap"> ' +
  144. ' <img class="image" /> ' +
  145. ' </a> ' +
  146. ' </div> '
  147. );
  148. $(document).hoverIntent({
  149. selector: 'a[href^="/users/"][href*="/pictures"], a[href^="https://fetlife.com/users/"][href*="/pictures"]',
  150. over: function(){
  151. var linkEl = this;
  152. var href = $(this).attr('href');
  153. href = href.replace('https://fetlife.com', '');
  154. // prevent non-image links
  155. if( href.split('/').length != 5 ) return;
  156. // prevent self-links
  157. if( $(linkEl).closest('#bfl-image').length > 0 ) return;
  158. // prevent 'next image'
  159. if( $(linkEl).children('.fake_img').length > 0 ) return;
  160. // reset the popup
  161. hideImagePopup();
  162. // show the popup
  163. var css = {
  164. top: $(linkEl).offset().top + $(linkEl).height() + 8
  165. }
  166. if( $(linkEl).offset().left > $(window).width()/2 ) {
  167. css.right = $(window).width() - $(linkEl).offset().left - $(linkEl).width();
  168. $('#bfl-image').addClass('alignright');
  169. } else {
  170. css.left = $(linkEl).offset().left;
  171. }
  172. $('#bfl-image')
  173. .addClass('loading')
  174. .css(css)
  175. .show();
  176. $.ajax({
  177. url: href,
  178. dataType: "html",
  179. success: function(html){
  180. var title = $(html).find('.s.i.caption').text();
  181. var likeUrl = href.split('/');
  182. likeUrl = likeUrl[ likeUrl.length-1 ];
  183. likeUrl = "/pictures/" + likeUrl + "/likes"
  184. // extract the image src
  185. var src = $(html).find('style').first().html().match(/\(\'(.*?)\'\)/);
  186. src = src[0];
  187. src = src.replace("('", "");
  188. src = src.replace("')", "");
  189. $('#bfl-image .title')
  190. .html(title)
  191. .attr('title', title)
  192. $('#bfl-image .like-wrap')
  193. .attr('data-href', likeUrl);
  194. $('#bfl-image .image-wrap')
  195. .attr('href', href);
  196. $('#bfl-image .image')
  197. .load(function(){
  198. $('#bfl-image').removeClass('loading')
  199. })
  200. .attr('src', src)
  201. // get amount of likes
  202. $.ajax({
  203. url: likeUrl,
  204. dataType: "json",
  205. success: function(data) {
  206. $('#bfl-image .like-wrap').toggle(data.user_can_like);
  207. if( data.is_liked_by_user ) {
  208. $('#bfl-image .like-wrap').addClass('liked');
  209. }
  210. $('#bfl-image .like-count')
  211. .html(data.total);
  212. }
  213. });
  214. }
  215. });
  216. },
  217. out: function(e){
  218. var linkEl = this;
  219. var href = $(linkEl).attr('href');
  220. // prevent non-user links
  221. if( href.split('/').length != 5 ) return;
  222. clearTimeout(hideImagePopupTimeout);
  223. hideImagePopupTimeout = setTimeout(hideImagePopup, hideImagePopupDelay);
  224. }
  225. });
  226. $(document).on('mouseover', '#bfl-image', function(e){
  227. clearTimeout(hideImagePopupTimeout);
  228. clearTimeout(hideUserPopupTimeout);
  229. });
  230. $(document).on('mouseleave', '#bfl-image', function(e){
  231. clearTimeout(hideImagePopupTimeout);
  232. hideImagePopupTimeout = setTimeout(hideImagePopup, hideImagePopupDelay);
  233. });
  234.  
  235. var hideImagePopupTimeout = setTimeout('', 0);
  236. var hideImagePopupDelay = 500;
  237. function hideImagePopup() {
  238. $('#bfl-image .title').html('').attr('href', '');
  239. $('#bfl-image .image').attr('src', '');
  240. $('#bfl-image .like-wrap').attr('data-href', '');
  241. $('#bfl-image .like-count').html('');
  242. $('#bfl-image').removeClass('loading');
  243. $('#bfl-image').removeClass('alignright');
  244. $('#bfl-image').hide();
  245. }
  246. $(document).on('click', '#bfl-image .like-wrap', function(){
  247. var this_ = this;
  248. $.ajax({
  249. url: $(this_).data('href') + '/toggle',
  250. type: 'post',
  251. success: function(){
  252. if( $(this_).hasClass('liked') ) {
  253. $('#bfl-image .like-count').html( parseInt( $('#bfl-image .like-count').html()) - 1 )
  254. } else {
  255. $('#bfl-image .like-count').html( parseInt( $('#bfl-image .like-count').html()) + 1 )
  256. }
  257. $(this_).toggleClass('liked');
  258. }
  259. });
  260. return false;
  261. });
  262. });
  263.  
  264. var bfl_css = '' +
  265. ' #bfl-user {' +
  266. ' position: absolute;' +
  267. ' z-index: 100;' +
  268. ' display: none;' +
  269. ' padding: 4px;' +
  270. ' min-width: 180px;' +
  271. ' height: 80px;' +
  272. ' padding-left: 92px;' +
  273. ' padding-right: 8px;' +
  274. ' background: #323232;' +
  275. ' border: 3px solid #171717;' +
  276. ' }' +
  277. ' #bfl-user.loading {' +
  278. ' padding-left: 84px;' +
  279. ' padding-right: 4px;' +
  280. ' min-width: 0;' +
  281. ' }' +
  282. ' #bfl-user:before,' +
  283. ' #bfl-image:before {' +
  284. ' position: absolute;' +
  285. ' z-index: 101;' +
  286. ' display: block;' +
  287. ' content: "";' +
  288. ' left: 7px;' +
  289. ' top: -8px;' +
  290. ' border: 8px solid transparent;' +
  291. ' border-bottom-color: #171717;' +
  292. ' border-top-width: 0;' +
  293. ' }' +
  294. ' #bfl-user:after,' +
  295. ' #bfl-image:after {' +
  296. ' position: absolute;' +
  297. ' z-index: 102;' +
  298. ' display: block;' +
  299. ' content: "";' +
  300. ' left: 10px;' +
  301. ' top: -5px;' +
  302. ' border: 5px solid transparent;' +
  303. ' border-bottom-color: #323232;' +
  304. ' border-top-width: 0;' +
  305. ' }' +
  306. ' #bfl-user .avatar {' +
  307. ' position: absolute;' +
  308. ' left: 4px;' +
  309. ' width: 80px;' +
  310. ' height: 80px;' +
  311. ' padding: 0px;' +
  312. ' margin-right: 8px;' +
  313. ' background-color: transparent;' +
  314. ' background-size: cover;' +
  315. ' background-position: center center;' +
  316. ' background-repeat: no-repeat;' +
  317. ' }' +
  318. ' #bfl-user.loading .avatar {' +
  319. ' background-size: auto;' +
  320. ' background-image: url(https://flassets.a.ssl.fastly.net/std/spinners/circle_big.gif);' +
  321. ' margin-right: 0;' +
  322. ' }' +
  323. ' #bfl-user .name {' +
  324. ' white-space: nowrap;' +
  325. ' }' +
  326. ' #bfl-user .status {' +
  327. ' white-space: nowrap;' +
  328. ' color: #aaa;' +
  329. ' }' +
  330. ' #bfl-user .location {' +
  331. ' display: block;' +
  332. ' font-size: 12px;' +
  333. ' white-space: nowrap;' +
  334. ' }' +
  335. ' #bfl-user .friends_status {' +
  336. ' float: right;' +
  337. ' font-size: 12px;' +
  338. ' }' +
  339. ' #bfl-user .photos {' +
  340. ' position: absolute;' +
  341. ' right: 4px;' +
  342. ' bottom: 4px;' +
  343. ' font-size: 12px;' +
  344. ' }' +
  345. ' #bfl-user .photos a {' +
  346. ' float: left;' +
  347. ' font-size: 12px;' +
  348. ' }' +
  349. ' #bfl-user .photos a img {' +
  350. ' float: left;' +
  351. ' margin: 2px;' +
  352. ' width: 25px;' +
  353. ' height: 25px;' +
  354. ' padding: 0;' +
  355. ' }' +
  356. ' #bfl-image {' +
  357. ' position: absolute;' +
  358. ' z-index: 100;' +
  359. ' display: none;' +
  360. ' background: #323232;' +
  361. ' border: 3px solid #171717;' +
  362. ' padding: 4px;' +
  363. ' }' +
  364. ' #bfl-image.alignright:before {' +
  365. ' left: auto;' +
  366. ' right: 7px;' +
  367. ' }' +
  368. ' #bfl-image.alignright:after {' +
  369. ' left: auto;' +
  370. ' right: 10px;' +
  371. ' }' +
  372. ' #bfl-image.loading {' +
  373. ' width: 80px;' +
  374. ' height: 80px;' +
  375. ' background: #323232 url(https://flassets.a.ssl.fastly.net/std/spinners/circle_big.gif) no-repeat center center;' +
  376. ' }' +
  377. ' #bfl-image .header {' +
  378. ' position: absolute;' +
  379. ' left: 0;' +
  380. ' right: 0;' +
  381. ' top: 0;' +
  382. ' background: #323232;' +
  383. ' padding: 4px;' +
  384. ' overflow: hidden;' +
  385. ' font-size: 12px;;' +
  386. ' }' +
  387. ' #bfl-image.loading .header {' +
  388. ' display: none;' +
  389. ' }' +
  390. ' #bfl-image .title {' +
  391. ' float: left;' +
  392. ' width: 80%;' +
  393. ' white-space: nowrap;' +
  394. ' overflow: hidden;' +
  395. ' text-overflow: ellipsis;' +
  396. ' }' +
  397. ' #bfl-image .like-wrap {' +
  398. ' float: right;' +
  399. ' width: 10%;' +
  400. ' white-space: nowrap;' +
  401. ' text-align: right;' +
  402. ' cursor: pointer;' +
  403. ' }' +
  404. ' #bfl-image .like-wrap:hover {' +
  405. ' color: #ffffff;' +
  406. ' }' +
  407. ' #bfl-image .like-wrap:active {' +
  408. ' color: #bbbbbb;' +
  409. ' }' +
  410. ' #bfl-image .like-wrap.liked {' +
  411. ' color: #DD0000;' +
  412. ' }' +
  413. ' #bfl-image .like-wrap.liked:hover {' +
  414. ' color: #FF0000;' +
  415. ' }' +
  416. ' #bfl-image .like-wrap.liked:active {' +
  417. ' color: #BB0000;' +
  418. ' }' +
  419. ' #bfl-image .image {' +
  420. ' display: block;' +
  421. ' padding: 0;' +
  422. ' }' +
  423. ' #bfl-image.loading .image {' +
  424. ' opacity: 0;' +
  425. ' }' +
  426.  
  427. /*!
  428. * hoverIntent v1.8.0 // 2014.06.29 // jQuery v1.9.1+
  429. * http://cherne.net/brian/resources/jquery.hoverIntent.html
  430. *
  431. * You may use hoverIntent under the terms of the MIT license. Basically that
  432. * means you are free to use hoverIntent as long as this header is left intact.
  433. * Copyright 2007, 2014 Brian Cherne
  434. */
  435. /* hoverIntent is similar to jQuery's built-in "hover" method except that
  436. * instead of firing the handlerIn function immediately, hoverIntent checks
  437. * to see if the user's mouse has slowed down (beneath the sensitivity
  438. * threshold) before firing the event. The handlerOut function is only
  439. * called after a matching handlerIn.
  440. *
  441. * // basic usage ... just like .hover()
  442. * .hoverIntent( handlerIn, handlerOut )
  443. * .hoverIntent( handlerInOut )
  444. *
  445. * // basic usage ... with event delegation!
  446. * .hoverIntent( handlerIn, handlerOut, selector )
  447. * .hoverIntent( handlerInOut, selector )
  448. *
  449. * // using a basic configuration object
  450. * .hoverIntent( config )
  451. *
  452. * @param handlerIn function OR configuration object
  453. * @param handlerOut function OR selector for delegation OR undefined
  454. * @param selector selector OR undefined
  455. * @author Brian Cherne <brian(at)cherne(dot)net>
  456. */
  457. (function($) {
  458. $.fn.hoverIntent = function(handlerIn,handlerOut,selector) {
  459.  
  460. // default configuration values
  461. var cfg = {
  462. interval: 100,
  463. sensitivity: 6,
  464. timeout: 0
  465. };
  466.  
  467. if ( typeof handlerIn === "object" ) {
  468. cfg = $.extend(cfg, handlerIn );
  469. } else if ($.isFunction(handlerOut)) {
  470. cfg = $.extend(cfg, { over: handlerIn, out: handlerOut, selector: selector } );
  471. } else {
  472. cfg = $.extend(cfg, { over: handlerIn, out: handlerIn, selector: handlerOut } );
  473. }
  474.  
  475. // instantiate variables
  476. // cX, cY = current X and Y position of mouse, updated by mousemove event
  477. // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
  478. var cX, cY, pX, pY;
  479.  
  480. // A private function for getting mouse position
  481. var track = function(ev) {
  482. cX = ev.pageX;
  483. cY = ev.pageY;
  484. };
  485.  
  486. // A private function for comparing current and previous mouse position
  487. var compare = function(ev,ob) {
  488. ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
  489. // compare mouse positions to see if they've crossed the threshold
  490. if ( Math.sqrt( (pX-cX)*(pX-cX) + (pY-cY)*(pY-cY) ) < cfg.sensitivity ) {
  491. $(ob).off("mousemove.hoverIntent",track);
  492. // set hoverIntent state to true (so mouseOut can be called)
  493. ob.hoverIntent_s = true;
  494. return cfg.over.apply(ob,[ev]);
  495. } else {
  496. // set previous coordinates for next time
  497. pX = cX; pY = cY;
  498. // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
  499. ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
  500. }
  501. };
  502.  
  503. // A private function for delaying the mouseOut function
  504. var delay = function(ev,ob) {
  505. ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
  506. ob.hoverIntent_s = false;
  507. return cfg.out.apply(ob,[ev]);
  508. };
  509.  
  510. // A private function for handling mouse 'hovering'
  511. var handleHover = function(e) {
  512. // copy objects to be passed into t (required for event object to be passed in IE)
  513. var ev = $.extend({},e);
  514. var ob = this;
  515.  
  516. // cancel hoverIntent timer if it exists
  517. if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
  518.  
  519. // if e.type === "mouseenter"
  520. if (e.type === "mouseenter") {
  521. // set "previous" X and Y position based on initial entry point
  522. pX = ev.pageX; pY = ev.pageY;
  523. // update "current" X and Y position based on mousemove
  524. $(ob).on("mousemove.hoverIntent",track);
  525. // start polling interval (self-calling timeout) to compare mouse coordinates over time
  526. if (!ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
  527.  
  528. // else e.type == "mouseleave"
  529. } else {
  530. // unbind expensive mousemove event
  531. $(ob).off("mousemove.hoverIntent",track);
  532. // if hoverIntent state is true, then call the mouseOut function after the specified delay
  533. if (ob.hoverIntent_s) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
  534. }
  535. };
  536.  
  537. // listen for mouseenter and mouseleave
  538. return this.on({'mouseenter.hoverIntent':handleHover,'mouseleave.hoverIntent':handleHover}, cfg.selector);
  539. };
  540. })(jQuery);