Sleazy Fork is available in English.

fetlife_all_members (ASL+role+status filter)

greasemonkey script to filter fetlife members when it's possible. Search by name, gender, role, age, location or status.

As of 25/11/2018. See the latest version.

  1. // ==UserScript==
  2. // @name fetlife_all_members (ASL+role+status filter)
  3. // @namespace io.github.bewam
  4. // @description greasemonkey script to filter fetlife members when it's possible. Search by name, gender, role, age, location or status.
  5. // @include https://fetlife.com/*
  6. // @version 1.9.2.20180609
  7. // @run-at document-end
  8. // @include-jquery false
  9. // @use-greasemonkey true
  10. // @require http://code.jquery.com/jquery-2.1.1.min.js
  11. // ==/UserScript==
  12.  
  13. // NOTE: comment (change first "/**/" to "/*") for debugging.
  14. console = { log: ()=>{}}; /**/
  15.  
  16. var useCurrentPageAsDefault = false;
  17. var onlyWithAvatar = false; //actual default see #4 on github
  18.  
  19. /* care modifing
  20. * TODO : to be removed: https://greasyfork.org/fr/forum/discussion/4199/lock-a-script#latest
  21. */
  22. const FETCH_LATENCY = 2000;
  23. // jshint ignore: start
  24.  
  25. const forceNoAvatar = true;// see #4 on github du to fetlife restriction + ajax
  26.  
  27. +(function ($) {
  28. // jshint ignore: end
  29. // jquery str
  30. const Selector = {
  31. currentPage: 'em.current',
  32. body: 'body',
  33. users: 'div.fl-member-card',
  34. pagination: "div.pagination",
  35. nextPage: 'a.next_page',
  36. nextDisabled: 'span.next_page.disabled',
  37. previousPage: 'a.previous_page',
  38. firstUser: '.fl-member-card:first',
  39. user: {
  40. body: '.fl-flag__body',
  41. imageAvatar: 'img.fl-avatar__img',
  42. firstSpan: '.fl-member-card__user',
  43. profileLink: 'a.fl-member-card__user',
  44. shortDesc: '.fl-member-card__info',
  45. /** age, gender, role */
  46. location: 'span.fl-member-card__location',
  47. into: 'span.fl-member-card__action span',
  48. }
  49. };
  50. // script_name avoid name/id conflicts
  51. const _STR = 'fetlife_all_members';
  52. const ARRAY_GENDER = [
  53. 'M',
  54. 'F',
  55. 'NB',
  56. 'CD/TV',
  57. 'MtF',
  58. 'FtM',
  59. 'TG',
  60. 'GF',
  61. 'GQ',
  62. 'NB',
  63. 'IS',
  64. 'B',
  65. 'FEM'
  66. ];
  67. const ARRAY_GENDER_LABEL = [
  68. 'Male',
  69. 'Female',
  70. 'Non-Binary',
  71. 'CD/TV',
  72. 'Trans-MtF', 'Trans-FtM',
  73. 'Transgender',
  74. 'Gender Fluid',
  75. 'Genderqueer',
  76. 'NB',
  77. 'Intersex',
  78. 'Butch',
  79. 'Femme'
  80. ];
  81. const ARRAY_ROLE = [
  82. 'Dom',
  83. 'Domme',
  84. 'Switch',
  85. 'sub',
  86. 'Master',
  87. 'Mistress',
  88. 'slave',
  89. 'kajira',
  90. 'kajirus',
  91. 'Top',
  92. 'bottom',
  93. 'Sadist',
  94. 'Masochist',
  95. 'Sadomasochist',
  96. 'Kinkster',
  97. 'Fetishist',
  98. 'Swinger',
  99. 'Hedonist',
  100. 'Exhibitionist',
  101. 'Voyeur',
  102. 'Sensualist',
  103. 'Princess',
  104. 'Slut',
  105. 'Doll',
  106. 'sissy',
  107. 'Rigger',
  108. 'Rope Top',
  109. 'Rope Bottom',
  110. 'Rope Bunny',
  111. 'Spanko',
  112. 'Spanker',
  113. 'Spankee',
  114. 'Furry',
  115. 'Leather Man',
  116. 'Leather Woman',
  117. 'Leather Daddy',
  118. 'Leather Top',
  119. 'Leather bottom',
  120. 'Leather boy',
  121. 'Leather girl',
  122. 'Leather Boi',
  123. 'Bootblack',
  124. 'Primal',
  125. 'Primal Predator',
  126. 'Primal Prey',
  127. 'Bull',
  128. 'cuckold',
  129. 'cuckquean',
  130. 'Ageplayer',
  131. 'Daddy',
  132. 'Mommy',
  133. 'Big',
  134. 'Middle',
  135. 'little',
  136. 'brat',
  137. 'babygirl',
  138. 'babyboy',
  139. 'pet',
  140. 'kitten',
  141. 'pup',
  142. 'pony',
  143. 'Evolving',
  144. 'Exploring',
  145. 'Vanilla',
  146. 'Undecided',
  147. 'Handler',
  148. 'Disciplinarian',
  149. 'Drag King',
  150. 'Drag Queen',
  151. 'Toy',
  152. 'Cougar',
  153. 'Middle',
  154. 'Hotwife',
  155. 'Cuckoldress',
  156. 'Leatherman',
  157. 'Leather Mommy',
  158. 'Leatherboy',
  159. 'Leathergirl',
  160. 'Leatherboi'
  161. ];
  162. const ARRAY_ROLE_LABEL = ARRAY_ROLE;
  163. const ARRAY_INTO_STATUS = [
  164. 'is into',
  165. 'is curious about'
  166. ];
  167. const ARRAY_INTO_ACTIVITY = [
  168. 'giving',
  169. 'receiving',
  170. 'watching',
  171. 'wearing',
  172. 'watching others wear',
  173. 'everything to do with it'
  174. ];
  175.  
  176. const marker = {
  177. folded: '>',
  178. unfolded: 'v'
  179. };
  180. const isFetishesPage = /^\/fetishes/.test(location.pathname);
  181. const imageMissingData =
  182. `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`;
  183. // NOTE: do not modify unless you know what you're doing.
  184. var overlay = (form) =>
  185. $(Selector.firstUser)
  186. .parents('.clearfix:first')
  187. .prepend(form)
  188. .length;
  189.  
  190. /** TODO @class to manage pages */
  191. // Pagination = {
  192. // _next: '',
  193. // _previous: '',
  194. // _currentNo: '',
  195. // _clientPageNo,
  196. // setNext: setNext,
  197. // getNext: getNext,
  198. // getPrevious :() => this._previous,
  199. // setPrevious : (str) => {this._previous = str;},
  200. // getCurrentNo: () => this._currentNo ,
  201. // setCurrentNo: (n) => {this._currentNo = n;},
  202. // };
  203. // var page = Object.create(Pagination);
  204.  
  205. var lastPageNumber,
  206. _pageCurrentNo,
  207. _clientPageNo,
  208. InputcurrentPageDefaultVal,
  209. _fromPage = -1,
  210. _toPage = -1,
  211. pageMin = 1,
  212. pageMax = 1,
  213. _pageNext = '';
  214.  
  215. /* balance columns */
  216. var alternColumn = 0,
  217. _shownCount = 0;
  218.  
  219. /* store user html block index is shared with mCache */
  220. /** NOTE see initUserCache() for desc. */
  221. var mCache = [],
  222. /** referrer (pages) */
  223. rCache = [],
  224. /** image avatar data */
  225. aCache = [],
  226. listContainers = [] // columns where lists appear
  227. ;
  228. //TODO Expressions =
  229. var userRegExp = new RegExp('([0-9]{2})(' + ARRAY_GENDER.join('|') +
  230. ')? (' +
  231. ARRAY_ROLE.join('|') + ')?', 'i');
  232. var regInto = new RegExp('^(' + ARRAY_INTO_STATUS.join('|') +
  233. ') ?(.*$)?');
  234.  
  235. /** modified, value, default value */
  236. var filters = {
  237. 'NameContains': [false, '', ''],
  238. 'AgeMin': [false, '', ''],
  239. 'AgeMax': [false, '', ''],
  240. 'Gender': [false, [], []],
  241. 'Role': [false, [], []],
  242. 'LocContains': [false, '', ''],
  243. 'IntoStatus': [false, '', ''],
  244. 'IntoActivity': [false, '', '']
  245. // has_avatar: @see function
  246. };
  247.  
  248. var ajaxLocked = false,
  249. scriptLaunched = false,
  250. stopped = false;
  251.  
  252. /* jshint ignore:start */
  253. addStyle(
  254. '#' + S('Wrapper') + ' { ' +
  255. // 'background-color: rgba(255, 255, 255, 0.4);' +
  256. 'border: solid 2px lightgray;' +
  257. 'color: white !important;/**/ ' +
  258. 'padding:5px;' +
  259. 'min-height:15px !important;' +
  260. 'vertical-align:middle;' +
  261. 'margin-bottom: 10px;' +
  262. '} ' +
  263. '#' + S('Controls') + ' { ' +
  264. 'display: block;/**/ ' +
  265. 'min-height:15px !important;' +
  266. '}' +
  267. '#' + S('Content') + ' { ' +
  268. 'display: none;/**/ ' +
  269. 'margin-top: 5px;' +
  270. '}' +
  271. '#' + S('Buttons') + '{ ' +
  272. 'margin-top: 10px;' +
  273. '}' +
  274. '#' + S('ButtonStop') + '{ ' +
  275. // 'display: none;' +
  276. '}' +
  277. `.flfm-has-avatar{
  278. border: solid 1px red;
  279. }`
  280. );
  281. /* jshint ignore:end */
  282.  
  283. /*-----------------------------------*/
  284.  
  285. /*----------------html related--------------------------*/
  286. function buildOptions(arr1, arr2) {
  287. var str = '';
  288. $.each(arr1, function (i, v) {
  289. str += '<option value="' + v + '" >' + arr2[i] +
  290. ' </option>';
  291. });
  292. return str;
  293. }
  294.  
  295. function addStyle(css) {
  296. var newStyleSheet = document.createElement('style');
  297. document.body.appendChild(newStyleSheet);
  298. newStyleSheet.textContent = css;
  299.  
  300. return newStyleSheet;
  301. }
  302.  
  303. function drawBlock() {
  304. var optionsGender = buildOptions(ARRAY_GENDER, ARRAY_GENDER_LABEL);
  305. var optionsRole = buildOptions(ARRAY_ROLE, ARRAY_ROLE_LABEL);
  306. var optionsIntoStatus = buildOptions(ARRAY_INTO_STATUS,
  307. ARRAY_INTO_STATUS);
  308. var optionsIntoActivity = buildOptions(ARRAY_INTO_ACTIVITY,
  309. ARRAY_INTO_ACTIVITY);
  310.  
  311. var BLOCK =
  312. '<div id="' + S('Wrapper') + '"> ' +
  313. ' <div id="' + S('Controls') + '">' +
  314. ' <b>&gt;</b>&nbsp;' +
  315. ' <u>filter members</u>' +
  316. ' </div>' +
  317. ' <div id="' + S('Content') + '"> ' +
  318. ' <div id="' + S('Filters') + '">' +
  319. ' <label for="' + S('NameContains') +
  320. '">name:&nbsp;&nbsp;&nbsp; </label>' +
  321. ' <input id="' + S('NameContains') +
  322. '" type="text" class="filter"></input>' +
  323. ' <br />' +
  324. ' <label for="' + S('LocContains') +
  325. '">location:</label>' +
  326. ' <input id="' + S('LocContains') +
  327. '" type="text" class="filter" ></input>' +
  328. ' <br />' +
  329. ' <u>age</u>&nbsp;' +
  330. ' <label for="' + S('AgeMin') + '">min:</label>' +
  331. ' <input id="' + S('AgeMin') +
  332. '" type="text" class="filter" size="2"></input>' +
  333. '&nbsp; ' +
  334. ' <label for="' + S('AgeMax') + '">max:</label>' +
  335. ' <input id="' + S('AgeMax') +
  336. '" type="text" class="filter" size="2"></input>' +
  337. ' <br />' +
  338. ' <small>' +
  339. ' Use CTRL key to multiple select.' +
  340. ' <br />' +
  341. ' Use MAJ key to do a range select.' +
  342. ' </small>' +
  343. ' <br />' +
  344. ' <label for="' + S('Gender') + '">gender:</label>' +
  345. ' <select id="' + S('Gender') +
  346. '" class="filter" name="gender" multiple="multiple" size="3">' +
  347. ' <option value="" selected="selected">none specified</option>' +
  348. optionsGender +
  349. ' </select>' +
  350. ' <label for="' + S('Role') + '">role:</label>' +
  351. ' <select id="' + S('Role') +
  352. '" class="filter" name="role" multiple="multiple" size="5">' +
  353. ' <option value="" selected="selected">none specified</option>' +
  354. optionsRole +
  355. ' </select>';
  356.  
  357. if(isFetishesPage) {
  358. BLOCK +=
  359. '&nbsp; Into &nbsp;' +
  360. ' <select id="' + S('IntoStatus') +
  361. '" multiple="multiple" size="3">' +
  362. ' <option value="" selected="selected">All</option>' +
  363. optionsIntoStatus +
  364. ' </select>' +
  365. ' <select id="' + S('IntoActivity') +
  366. '" multiple="multiple" size="3">' +
  367. ' <option value="" selected="selected">All</option>' +
  368. optionsIntoActivity +
  369. ' </select>';
  370. }
  371.  
  372. BLOCK +=
  373. ' <br />' +
  374. // ' <input type="hidden" name="' + S('HasAvatar') +
  375. // '" ></input>' +
  376. ' <input type="checkbox" id="' + S('HasAvatar') +
  377. '" name="' + S(
  378. 'HasAvatar') + '" ' + (onlyWithAvatar ? 'checked="true"' :
  379. 'false') + '></input>' +
  380. ' <label for="' + S('HasAvatar') +
  381. '">only with avatar</label>' +
  382. ' <br />' +
  383. ' <label for="' + S('FromPage') +
  384. '">From page:&nbsp;</label>' +
  385. ' <input id="' + S('FromPage') +
  386. '" type="text" class="filter" size="3" value="' +
  387. InputcurrentPageDefaultVal + '"></input>' +
  388. ' <input id="' + S('ButtonCurrentPage') +
  389. '" type="button" value="current"></input>' +
  390. ' <label for="' + S('ToPage') +
  391. '"> &nbsp;to page:&nbsp;</label>' +
  392. ' <input id="' + S('ToPage') + '" name="' + S('ToPage') +
  393. '" type="text" class="filter" size="3"></input>' +
  394. ' <br />' +
  395. ' <br />' +
  396. ' </div> ' + // Filters
  397. ' <div id="' + S('Buttons') + '" style="display:inline;">' +
  398. ' <input id="' + S('ButtonGo') +
  399. '" type="button" value="view&nbsp;all"></input>' +
  400. ' <input id="' + S('ButtonStop') +
  401. '" type="button" value="&nbsp;stop&nbsp;" disabled="true"></input>' +
  402. ' </div>' + // Buttons
  403. ' <div id="' + S('Info') + '">' +
  404. ' <br />' +
  405. ' <span id="' + S('ShowCount') + '">' +
  406. ' </span> ' +
  407. ' </div> ' + // Info
  408. ' </div> ' + //Content
  409. '</div>'; // Wrapper
  410. if(overlay(BLOCK) < 1) {
  411. $(Selector.body)
  412. .prepend(BLOCK);
  413. }
  414. }
  415.  
  416. function disableAllInput() {
  417. // TODO @class Form. Form.disableEntries() => void()
  418. $I('Content')
  419. .find('*')
  420. .attr("disabled", 'true');
  421. $I('ButtonStop')
  422. .removeAttr("disabled");
  423. }
  424.  
  425. function enableAllInput() {
  426. $I('Content')
  427. .find('*')
  428. .removeAttr("disabled");
  429. $I('ButtonStop')
  430. .attr("disabled", 'true');
  431. }
  432.  
  433. function disableInput() {
  434. // TODO @function Form.disableEntries() => void()
  435. $I('Content')
  436. .find('*')
  437. .attr("disabled", 'true');
  438. }
  439.  
  440. function enableInput() {
  441. $I('Content')
  442. .find('*')
  443. .removeAttr("disabled");
  444. }
  445. /**
  446. * @param str id
  447. * @return object jQuery with the tagged (script name) id
  448. */
  449. function $I(id) {
  450. var str = arguments.length > 1 ? arguments[1] : '';
  451. return $('#' + S(id) + str);
  452. }
  453.  
  454. function S(name) {
  455. return _STR + name;
  456. }
  457.  
  458. function rS(str) {
  459. return str.replace(_STR, '');
  460. }
  461.  
  462. function cleanPage() {
  463. $(Selector.users)
  464. .remove();
  465. $(Selector.pagination)
  466. .hide();
  467. _shownCount = 0;
  468. }
  469.  
  470. function toggleMarker($el) {
  471. var cnt = $el.html();
  472. $el.html(cnt == marker.unfolded ? marker.folded : marker.unfolded);
  473. }
  474. /*-----------------------------------*/
  475. /** Would grab 2 containers where to put members in */
  476. function initContainers(pageUsers) {
  477. var parent;
  478. $(pageUsers)
  479. .each(function () {
  480. parent = $(this)
  481. .parent()
  482. .get(0);
  483. console.log("container: " + $(parent)
  484. .attr("class"));
  485.  
  486. if($.inArray(parent, listContainers) == -1) {
  487. listContainers.push(parent);
  488. }
  489. });
  490. }
  491. /*------------------------------------*/
  492. function updateMemberFilters() {
  493. $I('Content')
  494. .find('select, input[type=text]')
  495. .each(function () {
  496.  
  497. var name = rS($(this)
  498. .attr('id'));
  499. console.log('updating filter: ' + name);
  500. if(!filters[name]) {
  501. return;
  502. }
  503. if(typeof filters[name][2] != 'string') {
  504. var options = $(this)
  505. .find('option:selected');
  506. filters[name][0] = false;
  507. filters[name][1] = [];
  508. $(options)
  509. .each(function () {
  510. filters[name][0] = true;
  511. filters[name][1].push($(this)
  512. .val());
  513. });
  514. if(filters[name][1].length == 1 && filters[name][1]
  515. [0] === '') {
  516. filters[name][0] = false;
  517. }
  518. } else
  519. if(filters[name][2] != $(this)
  520. .val()) {
  521. filters[name][0] = true;
  522. filters[name][1] = $(this)
  523. .val();
  524. } else {
  525. filters[name][0] = false;
  526. filters[name][1] = filters[name][2];
  527. }
  528. });
  529. console.log(filters);
  530. }
  531.  
  532. function updateAvatarFilter() {
  533. onlyWithAvatar = ($I('HasAvatar', ':checked')
  534. .length > 0);
  535. }
  536.  
  537. function updatePaginationFilters() {
  538. var reload = false;
  539. var $fromPage = parseInt($I('FromPage')
  540. .val());
  541. var $toPage = parseInt($I('ToPage')
  542. .val());
  543.  
  544.  
  545. if(_fromPage != $fromPage) {
  546. _fromPage = $fromPage;
  547. reload = true;
  548. }
  549.  
  550. if(_toPage != $toPage) {
  551. _toPage = $toPage;
  552. reload = true;
  553. }
  554.  
  555. console.log(' _fromPage ' + _fromPage + ' $fromPage ' + $fromPage);
  556. console.log(' _toPage ' + _toPage + ' $toPage ' + $toPage);
  557.  
  558. return reload;
  559. }
  560. /*-----------------------------------*/
  561. var Refining = function (cacheItem) {
  562.  
  563. var c = cacheItem;
  564. var filtered = false;
  565.  
  566. /**
  567. * @return bool false by default, element is not trapped */
  568. this.isFiltered = function () {
  569. // NOTE for debug purpose uncomment the second block and / comment this one.
  570. // TODO how secure is eval() ?
  571. /**
  572. role();
  573. name();
  574. location();
  575. hasAvatar();
  576. gender();
  577. ageMin();
  578. ageMax();
  579. intoStatus();
  580. intoActivity();
  581. /**/
  582.  
  583. /**/
  584. var functions = [
  585. "ageMax",
  586. "ageMin",
  587. "gender",
  588. "hasAvatar",
  589. "intoActivity",
  590. "intoStatus",
  591. "location",
  592. "name",
  593. "role"
  594. ];
  595. for(var fn of functions) {
  596. // jshint ignore : start
  597. eval(fn + '()');
  598. // jshint ignore : end
  599. if(getF()) {
  600. console.log('is filtered: ' + fn.toString());
  601. console.log(c);
  602. // stop filtering here
  603. return true;
  604. }
  605. }
  606. /**/
  607. return filtered;
  608. };
  609. /**
  610. * @arg boolean filtered ?
  611. * @return void(0)
  612. */
  613. function setF(F) {
  614. if(F) {
  615. filtered = true;
  616. }
  617. }
  618.  
  619. function getF() {
  620. return filtered;
  621. }
  622.  
  623. /** NOTE Each filter (except hasAvatar) check if filter was modified
  624. (updateMemberFilters) and look if the changes concern the current
  625. user. Filter must set false if ok.
  626. */
  627.  
  628. function ageMax() {
  629. if(filters.AgeMax[0]) {
  630. setF(c[1] > parseInt(filters.AgeMax[1]));
  631. }
  632. }
  633.  
  634. function ageMin() {
  635. if(filters.AgeMin[0]) {
  636. setF(c[1] < parseInt(filters.AgeMin[1]));
  637. }
  638. }
  639.  
  640. function gender() {
  641. if(filters.Gender[0]) {
  642. setF($.inArray(c[2], filters.Gender[1]) < 0);
  643. }
  644. }
  645.  
  646. function hasAvatar() {
  647. // console.log('avatar ' + onlyWithAvatar.toString());
  648. setF((onlyWithAvatar && !c[6]) ? true : false);
  649. }
  650.  
  651. function intoActivity() {
  652. if(filters.IntoActivity[0]) {
  653. setF($.inArray(c[8], filters.IntoActivity[1]) < 0);
  654. }
  655. }
  656.  
  657. function intoStatus() {
  658. if(filters.IntoStatus[0]) {
  659. setF($.inArray(c[7], filters.IntoStatus[1]) < 0);
  660. }
  661. }
  662.  
  663. function location() {
  664. if(filters.LocContains[0]) {
  665. setF(
  666. c[4].toLowerCase()
  667. .indexOf(
  668. filters.LocContains[1].toLowerCase()
  669. ) < 0
  670. );
  671. }
  672. }
  673.  
  674. function name() {
  675. if(filters.NameContains[0]) {
  676. setF(
  677. c[0].toLowerCase()
  678. .indexOf(
  679. filters.NameContains[1].toLowerCase()
  680. ) < 0
  681. );
  682. }
  683. }
  684.  
  685. function role() {
  686. if(filters.Role[0]) {
  687. setF($.inArray(c[3], filters.Role[1]) < 0);
  688. }
  689. }
  690. }; // Filter
  691.  
  692. /** filter each user */
  693. function filterUser(n) {
  694. // console.log('filtering n°' + n);
  695. var o;
  696. var filter = new Refining(mCache[n]);
  697.  
  698. return(filter.isFiltered());
  699.  
  700. }
  701. /*-----------Ajax & pagination -----------------*/
  702. /** @param mixed (str or jQ.), a link "next" in pagination */
  703. function setNext(mix) {
  704. console.log('setNext: ');
  705. console.log(mix);
  706. var next = '';
  707. if(mix.nodeName === 'A') {
  708. next = mix.href;
  709. } else if(typeof mix == 'string') {
  710. next = mix;
  711. } else if(
  712. (typeof mix == 'function' ||
  713. typeof mix == 'object') &&
  714. $(mix)
  715. .is('A')
  716. ) {
  717. next = mix.attr('href');
  718. }
  719. _pageNext = next;
  720. return(next);
  721. }
  722.  
  723. function getNext() {
  724. console.log(_pageNext);
  725. if(_pageNext === '' || _pageNext.match(/^\s*$/)) {
  726. return '';
  727. }
  728. if(_pageNext.indexOf("fetlife.com") < 0) {
  729. return 'https://fetlife.com/' + _pageNext;
  730. } else {
  731. return _pageNext;
  732. }
  733. }
  734.  
  735. function getCurrentPageNo() {
  736. return parseInt($(Selector.currentPage)
  737. .text());
  738. }
  739.  
  740. function getLastPageNum() {
  741. var $e = $(Selector.nextPage);
  742. if($e.length > 0) {
  743. return parseInt($e.prev()
  744. .text());
  745. }
  746. $e = $(Selector.nextDisabled);
  747. if($e.length > 0) {
  748. return parseInt($e.prev()
  749. .text());
  750. }
  751. return -1;
  752. }
  753.  
  754. function isLastFetchedPage(next) {
  755. var p = next.lastIndexOf('?');
  756. var search;
  757. var S;
  758. if(p > -1) {
  759. // url.search W/o leading ?
  760. search = next.substr(p + 1);
  761. } else {
  762. // no url.search, stop now
  763. // TODO throw warning
  764. return true;
  765. }
  766. S = search.split('&');
  767. for(var i = 0; i < S.length; i++) {
  768. if(S[i].substr(0, 5) === 'page=') {
  769. return(parseInt(S[i].substr(5)) >= parseInt(_toPage) + 1);
  770. }
  771. }
  772. // unknown case, stop now
  773. // TODO throw warning
  774. return true;
  775. }
  776.  
  777. function addUrlString(url, pageNb) {
  778. var S = location.search.toString();
  779. console.log('location.search: ' + S);
  780. if(S.length <= 0) {
  781. return url + "?page=" + pageNb;
  782. }
  783. if(S.indexOf('page=') > -1) {
  784. return url.replace(/page=\d+/, 'page=' + pageNb);
  785. } else
  786. return url + '&page=' + pageNb;
  787. // FIXME: return what ? return (url + '&page=' + pageNb);
  788. }
  789.  
  790. function fetchMembers(startPageNo) {
  791.  
  792. // if (typeof startPageNo === 'undefined')
  793. // seekingEnded();
  794.  
  795. var next;
  796.  
  797. if(startPageNo) {
  798. console.log('start fetching from Page: ' + startPageNo);
  799. if(parseInt(startPageNo) > 0) {
  800. setNext(addUrlString(location.href, startPageNo));
  801. }
  802. } else {
  803. startPageNo = false;
  804. }
  805.  
  806. next = getNext();
  807. console.log("trying to fetch: " + next);
  808.  
  809. if(
  810. next === '' ||
  811. next.match(/^\s*$/) ||
  812. next === void(0)
  813. ) {
  814. seekingEnded();
  815. return;
  816. }
  817.  
  818. if(!ajaxLocked) {
  819. ajaxLocked = true;
  820. $.ajax({
  821. url: next,
  822. dataType: 'html',
  823. useCache: false,
  824. success: (data) => {
  825. var pageIndex = storePage(next)
  826. onMembersPage(data, pageIndex);
  827. },
  828. error: function (event) {
  829. seekingEnded();
  830. console.error(event);
  831. }
  832. });
  833. }
  834. showCount();
  835. }
  836.  
  837. function onMembersPage(data, pIndex) {
  838. var aNext = $(data)
  839. .find(Selector.nextPage);
  840.  
  841. setNext(aNext);
  842.  
  843. console.log("next page to fetch: " +
  844. getNext());
  845. var users = $(data)
  846. .find(Selector.users);
  847.  
  848. $.each(users, (i, user) => {
  849. var cacheIndex = initUserCache();
  850. mCache[cacheIndex][9] = pIndex;
  851. show(storeUser(cacheIndex, user));
  852. });
  853.  
  854. if(!stopped && !isLastFetchedPage(getNext())) {
  855. setTimeout(
  856. function () {
  857. ajaxLocked = false;
  858. fetchMembers(false);
  859. }, (FETCH_LATENCY < 0 ? FETCH_LATENCY : 0)
  860. );
  861. } else {
  862. seekingEnded();
  863. }
  864. }
  865.  
  866. function storePage(url) {
  867. return(rCache.push(url) - 1);
  868. }
  869.  
  870. function storeAvatar(src, userIndex) {
  871.  
  872. var C = mCache[userIndex];
  873. var referrer = rCache[C[9]];
  874.  
  875.  
  876. if(! C[6]) {
  877. console.log('user "', C[0], '" has no avatar');
  878. aCache[userIndex] = false;
  879. return;
  880. }
  881.  
  882. $.ajax({
  883. url: src,
  884. type: "GET",
  885. headers: {
  886. "X-Alt-Referer": referrer
  887. },
  888. crossDomain: true,
  889. xhrFields: {
  890. withCredentials: true,
  891. // responseType: 'blob'
  892. },
  893. success: function (data) {
  894. aCache[userIndex] = btoa(data);
  895.  
  896. imgData = getAvatarData(userIndex);
  897. $('body')
  898. .prepend('img')
  899. .attr('src', imgData)
  900. },
  901. error: function (event) {
  902. seekingEnded();
  903. console.error(event);
  904. }
  905. })
  906. .done(function () {
  907. console.log("img success");
  908. })
  909. .fail(function () {
  910. console.log("img error");
  911. })
  912. .always(function () {
  913. console.log("img complete");
  914. });
  915. }
  916.  
  917. function showInfo(str) {
  918. $I('ShowCount')
  919. .html(str);
  920. }
  921.  
  922. function showCount() {
  923. showInfo("members: " + _shownCount + " of " + mCache.length);
  924. }
  925.  
  926. function show(n) {
  927. var html;
  928. if(!filterUser(n)) {
  929. html = drawMemberCard(n)
  930. $(listContainers[alternColumn])
  931. .append(html)
  932. // console.log(html);
  933. _shownCount++;
  934. alternColumn = (alternColumn == (listContainers.length - 1)) ?
  935. 0 : (
  936. alternColumn + 1);
  937. }
  938. }
  939.  
  940. function drawMemberCard(userIndex) {
  941. /* 0:name, 1:age, 2:gender, 3:role, 4:location, 5:profileURL, 6:hasAvatar, 7:status, 8:activity */
  942. var C = mCache[userIndex];
  943. var avatar = getAvatarData(userIndex);
  944. var name = C[0];
  945. var age = C[1];
  946. var gender = C[2];
  947. var role = C[3];
  948. var location = C[4];
  949. var profileURL = C[5];
  950. var hasAvatarClass = C[6] ? 'flfm-has-avatar':'';
  951. var status = C[7];
  952. var activity = C[8];
  953. var memberCardHTML =
  954. `<div class="fl-member-card fl-flag">
  955. <div class="fl-flag__image ${hasAvatarClass}">
  956. <a class="fl-avatar__link" href="${profileURL}">
  957. <img alt="${name}" title="${name}" class="fl-avatar__img" src="${avatar}" width="73" height="73"></a>
  958. </div>
  959. <div class="fl-flag__body">
  960. <a class="fl-member-card__user" href="${profileURL}">${name}</a>
  961. <span class="fl-member-card__info">
  962. ${age}${gender}
  963. ${role}
  964. </span>
  965. <span class="fl-member-card__location">
  966. ${location}
  967. </span>`;
  968. memberCardHTML += isFetishesPage ?
  969. `
  970. <div class="fl-member-card__action">
  971. <span class="quiet small">
  972. ${status} ${activity}
  973. </span>
  974. </div>` :
  975. '';
  976. memberCardHTML += `</div>
  977. </div>`;
  978. return memberCardHTML;
  979. }
  980. // TODO for 2.0
  981. // var Cache = function () {}
  982. //
  983. // var Member = function () {
  984. //
  985. // htmlCache = [];
  986. //
  987. //
  988. // var isfilter = function (n) { Refining.isFiltered()}
  989. // var store = function () {};
  990. //
  991. // this.add = function () {};
  992. // this.show = function () {};
  993. // this.get = function (n) {};
  994. //
  995. // return this;
  996. // };
  997. /**
  998. 0:name, 1:age, 2:gender, 3:role, 4:location, 5:profileURL, 6:hasAvatar, 7:status, 8:activity, 9(internal): pageCacheIndex
  999. **/
  1000. function initUserCache() {
  1001. var defaults = ['', 0, '', '', '', '', false, '', '', -1];
  1002. var cacheLen = mCache.push(defaults);
  1003. var userIndex = cacheLen - 1;
  1004. return userIndex;
  1005. }
  1006.  
  1007. function getAvatarData(userIndex) {
  1008. var data = imageMissingData;
  1009.  
  1010. if(mCache[userIndex][6]) {
  1011. data = aCache[userIndex];
  1012. }
  1013. return "data:image/jpg;base64," + data;
  1014. }
  1015.  
  1016. function storeUser(cacheIndex, userData) {
  1017.  
  1018. var matches = [];
  1019. var firstSpan = $(userData)
  1020. .find(Selector.user.firstSpan);
  1021. var C, into, src;
  1022. var $avatar = $(userData)
  1023. .find(Selector.user.imageAvatar);
  1024.  
  1025. C = mCache[cacheIndex];
  1026.  
  1027. /* name */
  1028. C[0] = firstSpan.text();
  1029. /** profile url */
  1030. C[5] = $(userData)
  1031. .find(Selector.user.profileLink)
  1032. .attr('href');
  1033.  
  1034. src = $avatar.attr('src')
  1035. console.log(src);
  1036.  
  1037. /** hasAvatar, need to be false if user has */
  1038. if(src.indexOf('/images/avatar_missing') < 0 ) {
  1039. C[6] = true;
  1040. if (! forceNoAvatar ) {
  1041. storeAvatar(src, cacheIndex);
  1042. }
  1043. };
  1044. try {
  1045. var shortDesc = $(userData)
  1046. .find(Selector.user.shortDesc)
  1047. .text()
  1048. .replace(/\n|\r/g, ' ')
  1049. .replace(/\s{1,}|\n|\r/g, ' ');
  1050. console.log(shortDesc)
  1051. var matches = shortDesc.match(userRegExp);
  1052.  
  1053. if(matches.length < 2) {
  1054. return;
  1055. }
  1056. /* age */
  1057. C[1] = matches[1] || '';
  1058. /* gender */
  1059. C[2] = matches[2] || '';
  1060. /* role */
  1061. C[3] = matches[3] || '';
  1062. /* location*/
  1063. C[4] = $(userData)
  1064. .find(Selector.user.location)
  1065. .text() || '';
  1066. /* into */
  1067. if(isFetishesPage) {
  1068. matches = []; /* match: ["into status", "rest aka into activity" ] */
  1069. into = $(userData)
  1070. .find(Selector.user.into)
  1071. .text() || '';
  1072. // console.log('into: ' + into);
  1073. matches = into.match(regInto);
  1074. // console.log(matches);
  1075. if(matches) {
  1076. C[7] = matches[1] || '';
  1077. C[8] = matches[2] || '';
  1078. }
  1079. // console.log("mCache[i][7] = "+C[7]+" && mCache[i][8] = "+C[8])
  1080. }
  1081. } catch(err) {
  1082. console.log(err.message);
  1083. throw(new Error(err.message));
  1084. }
  1085. console.log(C);
  1086. return cacheIndex;
  1087. }
  1088.  
  1089. function initCaches() {
  1090. // referrer data to get avatars
  1091. rCache = [];
  1092. // avatars DATA
  1093. aCache = [];
  1094. // members
  1095. mCache = [];
  1096. }
  1097.  
  1098. function clearCache() {
  1099. // referrer data to get avatars
  1100. rCache = [];
  1101. // avatars DATA
  1102. aCache = [];
  1103. // members
  1104. mCache = [];
  1105. }
  1106.  
  1107. function gC(n) {
  1108. return mCache[n];
  1109. }
  1110.  
  1111. function getCache(n) {
  1112. return gC(n);
  1113. }
  1114. /*--------------helpers---------------*/
  1115. function isInt(n) {
  1116. return(!isNaN(n));
  1117. }
  1118.  
  1119. /*--------------Actions--------------*/
  1120. function init() {
  1121. var $pageNext = $(Selector.nextPage);
  1122. var $previousPage = $(Selector.previousPage);
  1123. var $pageUsers = $(Selector.users);
  1124. var $fromPage = $I('FromPage');
  1125.  
  1126. /** next_page link and list of members are on current page ? go on */
  1127. if(($pageNext.length > 0 || $previousPage.length > 0) &&
  1128. $pageUsers.length > 0
  1129. ) {
  1130. _clientPageNo = _pageCurrentNo = getCurrentPageNo();
  1131. initContainers($pageUsers);
  1132.  
  1133. console.log('_pageCurrentNo :' + _pageCurrentNo);
  1134.  
  1135. drawBlock();
  1136. $I('FromPage')
  1137. .val(
  1138. useCurrentPageAsDefault ?
  1139. _pageCurrentNo :
  1140. 1
  1141. );
  1142. pageMax = lastPageNumber = getLastPageNum();
  1143. console.log("lastPageNumber: " + lastPageNumber);
  1144. $I('ToPage')
  1145. .val(lastPageNumber);
  1146. setNext($pageNext);
  1147. initListener();
  1148. }
  1149. }
  1150.  
  1151. function showFilterAgain() {
  1152.  
  1153. $I('ButtonGo')
  1154. .val("filter again");
  1155. enableAllInput();
  1156.  
  1157. $I('ButtonGo')
  1158. .bind('click', () => {
  1159. if(updatePaginationFilters()) {
  1160. launchAgain();
  1161. $(this)
  1162. .unbind('click');
  1163. } else {
  1164. filterAgain();
  1165. }
  1166. });
  1167. }
  1168.  
  1169. function initListener() {
  1170. $I('Controls')
  1171. .click(function () {
  1172. $I('Content')
  1173. .toggle("slow");
  1174. toggleMarker($(this)
  1175. .find('b:first'));
  1176. });
  1177. $I('ButtonCurrentPage')
  1178. .click(function () {
  1179. $I('FromPage')
  1180. .val(_pageCurrentNo);
  1181. });
  1182. $I('ButtonGo')
  1183. .click(function () {
  1184. $(this)
  1185. .unbind('click');
  1186. launchSearch();
  1187.  
  1188. });
  1189. $I('ButtonStop')
  1190. .click(function () {
  1191. // $(this).unbind('click');
  1192. stopped = true;
  1193. });
  1194. }
  1195.  
  1196. function launchSearch() {
  1197. if(!scriptLaunched) {
  1198. updatePaginationFilters();
  1199. console.log("_fromPage: " + _fromPage);
  1200. console.log("_toPage: " + _toPage);
  1201. showInfo("loading ...");
  1202. updateMemberFilters();
  1203. updateAvatarFilter();
  1204. cleanPage();
  1205. disableAllInput();
  1206. fetchMembers(_fromPage);
  1207. scriptLaunched = true;
  1208. }
  1209. }
  1210.  
  1211. function launchAgain() {
  1212. cleanPage();
  1213. clearCache();
  1214. scriptLaunched = false;
  1215. launchSearch();
  1216. }
  1217.  
  1218. function filterAgain() {
  1219. alternColumn = 0;
  1220. // window.scrollTo(0, 0);
  1221. updateMemberFilters();
  1222. updateAvatarFilter();
  1223. cleanPage();
  1224. for(var i = 0; i < mCache.length; i++) {
  1225. show(i);
  1226. }
  1227. showCount();
  1228. }
  1229. /** due to recursive function*/
  1230. function seekingEnded() {
  1231. console.log("total members: " + mCache.length);
  1232. ajaxLocked = false;
  1233. stopped = false;
  1234. enableAllInput();
  1235. showFilterAgain();
  1236. showCount();
  1237. }
  1238. /* jshint ignore:start */
  1239. var count = 0;
  1240. if(typeof $ == 'function') {
  1241. init();
  1242. } else {
  1243. setTimeout(function () {
  1244. if(typeof $ !== 'function') {
  1245. alert('fetlife is modified, script ' +
  1246. GM_info.script.name +
  1247. 'can\'t run please contact the author.');
  1248. }
  1249. }, 3000);
  1250. }
  1251. /*-----------------------------------*/
  1252. })(jQuery);
  1253. jQuery.noConflict(true);
  1254. /* jshint ignore:end */