FastTech Forum Enhancements

Improvements for the FastTech forums and store

  1. // ==UserScript==
  2. // @name FastTech Forum Enhancements
  3. // @namespace ftil
  4. // @description Improvements for the FastTech forums and store
  5. // @include https://*.fasttech.com/*
  6. // @version 2.5.1
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @run-at document-start
  10. // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAOVBMVEVMaXEega+SlJeSkpgggK+Tk5gfgq+SlJeSkpkegK8egK8fgK+Tk5d1jZ6SkpcfgLEegLCTlJgfgbBPYHHZAAAAEXRSTlMApS2cl95Qi1CMvOmzFGgsdPNojJcAAACSSURBVHgBzdJlAgIwCAbQb915/7vaumG3vt8DRuAmIwheJOGQ2ywCvs8qYMVgsFLZSuiBrWhc4jsD8cIDrgh++qDNDM49MHmP4+wDDuIPHyRrE04VXbBlqZQ0hdhmKqXQZwwQaogcKD4MEt+QDMGLo8BvHe3qQTwwAJJjgyajPu/9Dwoj5MmDky6SEAlElYPGTUsirRDoa4UEWgAAAABJRU5ErkJggg==
  11. // ==/UserScript==
  12. 'use strict';
  13. // Delay function calls until their preconditions have been met.
  14. var block = (function() {
  15. var counters = { };
  16.  
  17. return {
  18. get: function(name) { return counters[name].val !== 0; },
  19.  
  20. block: function(name) {
  21. var c = counters[name];
  22. if (c === undefined)
  23. counters[name] = { val: 1, watches: [] };
  24. else if (c.val !== 0)
  25. c.val++;
  26. },
  27.  
  28. unblock: function(name) {
  29. var c = counters[name];
  30. if (c.val !== 0)
  31. c.val--;
  32. if (c.val === 0) {
  33. var w = c.watches;
  34. while (w.length !== 0)
  35. w.shift()();
  36. }
  37. },
  38.  
  39. watch: function(name, func) {
  40. var c = counters[name];
  41. if (c.val !== 0)
  42. c.watches.push(func);
  43. else
  44. func();
  45. },
  46. };
  47. })();
  48. /* Userscript settings:
  49. * This is just terrible. GM_[sg]etValue is used to initialize settings since
  50. * it's shared across subdomains, while localStorage is used to notify other
  51. * tabs of new settings.
  52. */
  53. var settings = (function() {
  54. var settings = { };
  55. var watches = { };
  56. var prefix = 'ignore_list:';
  57. var prere = new RegExp('^' + prefix + '(.+)');
  58. var have_ls = (function() {
  59. try {
  60. localStorage.getItem('test');
  61. return true;
  62. } catch (e) {
  63. return false;
  64. }
  65. })();
  66.  
  67. // Unblock after execution of the script
  68. block.block('ready');
  69.  
  70. function inner_set(name, value) {
  71. // Ugh.
  72. if (JSON.stringify(settings[name].value) === JSON.stringify(value))
  73. return false;
  74. settings[name].value = value;
  75. if (name in watches) {
  76. var w = watches[name];
  77. for (var i = 0; i < w.length; i++)
  78. w[i](name, value);
  79. }
  80. return true;
  81. }
  82.  
  83. if (have_ls) {
  84. window.addEventListener('storage', function(e) {
  85. var set = e.key.match(prere);
  86. if (set !== null && set[1] in settings)
  87. inner_set(set[1], JSON.parse(e.newValue));
  88. });
  89. }
  90.  
  91. function get_sync(name) {
  92. var val = GM_getValue(name, null);
  93. if (val !== null)
  94. return JSON.parse(val);
  95. else
  96. return settings[name].def;
  97. }
  98.  
  99. return {
  100. get: function(name) { return settings[name].value; },
  101. get_sync: get_sync,
  102.  
  103. set: function(name, value) {
  104. if (inner_set(name, value)) {
  105. var s = JSON.stringify(value);
  106. if (have_ls)
  107. localStorage.setItem(prefix + name, s);
  108. GM_setValue(name, s);
  109. }
  110. },
  111.  
  112. all: function() { return settings; },
  113.  
  114. register: function(name, desc, def, dep, opt) {
  115. settings[name] = {
  116. desc: desc,
  117. def: def,
  118. dep: dep,
  119. opt: opt,
  120. };
  121. inner_set(name, get_sync(name));
  122. },
  123.  
  124. watch: function(name, func) {
  125. if (!(name in watches))
  126. watches[name] = [];
  127. watches[name].push(func);
  128. if (settings[name] !== undefined)
  129. func(name, settings[name].value);
  130. },
  131.  
  132. update: function() { block.unblock('ready'); },
  133. };
  134. })();
  135. // Dynamic stylesheets
  136. var styles = (function() {
  137. var urls = {};
  138. var txts = {};
  139. var elms = {};
  140. var active_elm;
  141.  
  142. function try_gmgrt(name) {
  143. try {
  144. return GM_getResourceText('theme_' + name);
  145. } catch (e) {}
  146. }
  147.  
  148. function apply() {
  149. var enable = settings.get('use_theme');
  150. var name = settings.get('theme_name');
  151.  
  152. if (enable === true && name !== undefined) {
  153. if (active_elm !== undefined) {
  154. if (active_elm === elms[name])
  155. return;
  156. active_elm.disabled = true;
  157. }
  158.  
  159. if (name in elms) {
  160. elms[name].disabled = false;
  161. } else {
  162. var e, u;
  163. var c = try_gmgrt(name);
  164. if (c === undefined || c === null) {
  165. u = urls[name];
  166. c = txts[name];
  167. }
  168. if (c !== undefined) {
  169. e = document.createElement('style');
  170. e.type = 'text/css';
  171. e.innerHTML = c;
  172. } else {
  173. e = document.createElement('link');
  174. e.rel = 'stylesheet';
  175. e.type = 'text/css';
  176. e.href = u;
  177. }
  178. document.head.appendChild(e);
  179. elms[name] = e;
  180. }
  181.  
  182. active_elm = elms[name];
  183. } else if (active_elm !== undefined) {
  184. active_elm.disabled = true;
  185. active_elm = undefined;
  186. }
  187. }
  188.  
  189. return {
  190. register: function(name, url, txt) {
  191. if (url !== undefined)
  192. urls[name] = url;
  193. if (txt !== undefined)
  194. txts[name] = txt;
  195. },
  196.  
  197. update: function() {
  198. settings.watch('use_theme', apply);
  199. settings.watch('theme_name', apply);
  200. },
  201. };
  202. })();
  203. // Sanitize external values that may be fed into an innerHTML
  204. var strip_tags = (function() {
  205. var div = document.createElement('div');
  206. return function(t) {
  207. div.innerHTML = t;
  208. return div.textContent;
  209. };
  210. })();
  211.  
  212. // Case-insensitive indexOf (needed in case of case errors in user input)
  213. function ci_indexof(a, n) {
  214. var l = n.toLowerCase();
  215. for (var i = 0; i < a.length; i++) {
  216. if (a[i].toLowerCase() === l)
  217. return i;
  218. }
  219. return -1;
  220. }
  221.  
  222. // Case-insensitive attribute lookup
  223. function ci_lookup(a, n) {
  224. var l = n.toLowerCase();
  225. for (var k in a) {
  226. if (k.toLowerCase() === l)
  227. return k;
  228. }
  229. }
  230. // Assorted generic handlers for user input.
  231. var handlers = (function() {
  232. function Handler(funcs, elm, data, extra) {
  233. this.elm = elm;
  234. this.data = data;
  235. this.extra = extra;
  236. if (funcs.setting !== undefined)
  237. settings.watch(data, funcs.setting.bind(this));
  238. for (var k in funcs) {
  239. if (k.slice(0, 2) === 'on')
  240. elm.addEventListener(k.slice(2), funcs[k].bind(this));
  241. }
  242. }
  243.  
  244. function handler(fs) {
  245. return function(e, d, x) { return new Handler(fs, e, d, x); };
  246. }
  247.  
  248. return {
  249. generic: handler,
  250.  
  251. ignore_toggle: handler({
  252. onclick: function() { ignore_list.set(this.data); }
  253. }),
  254.  
  255. checkbox: handler({
  256. onchange: function() { settings.set(this.data, this.elm.checked); },
  257. setting: function(n, v) { this.elm.checked = v; }
  258. }),
  259.  
  260. top_click: handler({
  261. onclick: function() { scrollTo(0, 0); }
  262. }),
  263.  
  264. hide_toggle: handler({
  265. onclick: function() { unhide_posts.set(this.data); }
  266. }),
  267. };
  268. })();
  269. // Manage the state of the ignore list
  270. function create_ignore_list(list_var) {
  271. // Use two stages so that filters can handle updates before being called
  272. var watches = { 'early': [], 'late': [] };
  273. var il = [];
  274.  
  275. function fire_watches(user, state, idx) {
  276. ['early', 'late'].forEach(function(stage) {
  277. var w = watches[stage];
  278. for (var i = 0; i < w.length; i++)
  279. w[i](user, state, idx);
  280. });
  281. }
  282.  
  283. function inner_set(user, state, idx) {
  284. // In order to support userscripts, we need to rebuild the list on every set
  285. if (settings.get_sync) {
  286. il = [];
  287. var l = settings.get_sync(list_var);
  288. for (var i = 0; i < l.length; i++)
  289. il.push(l[i]);
  290. }
  291. if (state)
  292. il.push(user);
  293. else
  294. il.splice(idx, 1);
  295. settings.set(list_var, il);
  296. fire_watches(user, state, idx);
  297. }
  298.  
  299. settings.register(list_var, null, [], null);
  300. // Watch for ignorelist changes, and fire incremental watch events
  301. settings.watch(list_var, function(name, val) {
  302. var i, j;
  303. if (il.length === val.length)
  304. return;
  305. if (il.length === 0) {
  306. for (i = 0; i < val.length; i++)
  307. il.push(val[i]);
  308. fire_watches(il, true);
  309. return;
  310. }
  311. i = 0;
  312. j = 0;
  313. while (true) {
  314. if (il[i] === undefined && val[j] === undefined)
  315. break;
  316. if (il[i] !== val[j]) {
  317. var user, state, idx;
  318. if (il[i] === undefined) {
  319. // New entry @ end of list: user added
  320. user = val[j];
  321. il.push(user);
  322. state = true;
  323. idx = i;
  324. } else {
  325. // Missing entry inside list: user removed
  326. user = il[i];
  327. il.splice(i, 1);
  328. state = false;
  329. idx = i;
  330. }
  331. fire_watches(user, state, idx);
  332. continue;
  333. }
  334. i++;
  335. j++;
  336. }
  337. });
  338.  
  339. return {
  340. // Get ignored state of user (true == ignored)
  341. get: function(user) { return (ci_indexof(il, user) !== -1); },
  342.  
  343. // Set ignored state of user (undefined == toggle)
  344. set: function(user, state) {
  345. var idx = ci_indexof(il, user);
  346. if (state === undefined)
  347. state = (idx === -1);
  348. if ((state && (idx === -1)) || (!state && (idx !== -1)))
  349. inner_set(user, state, idx);
  350. },
  351.  
  352. /* Watch the ignorelist for changes (stage == ('early'|'late')).
  353. * func may be called with a single user and a new state, or an array of
  354. * users (with implicit ignored state for all)
  355. */
  356. watch: function(stage, func) {
  357. watches[stage].push(func);
  358. if (il !== [])
  359. func(il, true, -1);
  360. },
  361. };
  362. }
  363. /* Core post filtering logic
  364. * post vars are expected to be in the format
  365. * { user: string, content_elm: element, post_elms: [element] }
  366. */
  367. var post_filter = (function() {
  368. var posts;
  369. var watches = [];
  370. var filters;
  371.  
  372. settings.register('hide_ignored', 'Hide posts by ignored users', true, null);
  373. block.block('filter');
  374.  
  375. function update() {
  376. if (posts === undefined)
  377. return;
  378.  
  379. for (var i = 0; i < posts.length; i++) {
  380. var post = posts[i];
  381. var hide = false;
  382. var j;
  383. for (j = 0; j < filters.length; j++) {
  384. var tmp = filters[j](post);
  385. if (tmp === true) {
  386. hide = true;
  387. } else if (tmp === false) {
  388. hide = false;
  389. break;
  390. }
  391. }
  392.  
  393. var display = hide ? 'none' : '';
  394. for (j = 0; j < post.post_elms.length; j++)
  395. post.post_elms[j].style.display = display;
  396.  
  397. for (j = 0; j < watches.length; j++)
  398. watches[j](post, hide);
  399. }
  400.  
  401. block.unblock('filter');
  402. }
  403.  
  404. function builtin() {
  405. filters = [];
  406. filters.unshift((function() {
  407. settings.watch('hide_ignored', post_filter.update);
  408. ignore_list.watch('late', post_filter.update);
  409.  
  410. return function(post) {
  411. if (settings.get('hide_ignored') && ignore_list.get(post.user))
  412. return true;
  413. };
  414. })());
  415. unhide_posts = (function() {
  416. var posts = [];
  417.  
  418. filters.unshift(function(post) {
  419. if (posts.indexOf(post) !== -1)
  420. return false;
  421. });
  422.  
  423. return {
  424. // Set the unhide state of a post (true == unhide, undefined == toggle)
  425. set: function(post, state) {
  426. var idx = posts.indexOf(post);
  427. if (state === undefined)
  428. state = (idx === -1);
  429. if (state && idx === -1)
  430. posts.push(post);
  431. else if (!state && idx !== -1)
  432. posts.splice(idx, 1);
  433. else
  434. return;
  435. post_filter.update();
  436. }
  437. };
  438. })();
  439. }
  440.  
  441. return {
  442. // Register a list of posts to be filtered
  443. register: function(list) {
  444. posts = list;
  445. update();
  446. },
  447.  
  448. /* Add a new filter. Filters return true (should hide), false (must not
  449. * hide), or undefined (no judgement). Does not trigger update().
  450. */
  451. filter: function(func) {
  452. if (filters === undefined)
  453. builtin();
  454. filters.push(func);
  455. },
  456. // Run all filters again
  457. update: update,
  458.  
  459. /* Call func with changes to the hidden status of each post (true ==
  460. * hidden). Does not trigger update().
  461. */
  462. watch: function(func) { watches.push(func); },
  463. };
  464. })();
  465.  
  466. var unhide_posts;
  467. // Generic buttons to toggle ignored state of a user
  468. var ignore_buttons = (function() {
  469. var posts;
  470. var inner_ign = '';
  471. var inner_unign = '';
  472.  
  473. function update_btns(users, state) {
  474. var i;
  475. if (users instanceof Array) {
  476. for (i = 0; i < posts.length; i++) {
  477. posts[i].ignbtn.innerHTML =
  478. ci_indexof(users, posts[i].user) !== -1 ? inner_unign : inner_ign;
  479. }
  480. } else {
  481. var ul = users.toLowerCase();
  482. for (i = 0; i < posts.length; i++) {
  483. if (!ul.localeCompare(posts[i].user.toLowerCase()))
  484. posts[i].ignbtn.innerHTML = state ? inner_unign : inner_ign;
  485. }
  486. }
  487. }
  488.  
  489. return {
  490. // Set the (HTML) format of the (un)ignore buttons
  491. set: function(ign, unign) {
  492. inner_ign = ign;
  493. inner_unign = unign;
  494. },
  495.  
  496. // Register posts to have their ignore buttons handled
  497. register: function(p) {
  498. posts = p;
  499. for (var i = 0; i < p.length; i++)
  500. handlers.ignore_toggle(p[i].ignbtn, p[i].user);
  501. ignore_list.watch('late', update_btns);
  502. },
  503. };
  504. })();
  505. // Handle RES-style tagging of users
  506. var user_tags = (function() {
  507. var posts;
  508.  
  509. function update_badges(n, val) {
  510. for (var i = 0; i < posts.length; i++) {
  511. var post = posts[i];
  512. var k = ci_lookup(val, post.user);
  513. post.badge.innerHTML = k !== undefined ? val[k] : post.saved_badge;
  514. }
  515. }
  516.  
  517. var tag_prompt = handlers.generic({ onclick: function(e) {
  518. e.preventDefault();
  519. e.stopPropagation();
  520.  
  521. var user = this.data;
  522. var tmp =
  523. (settings.get_sync ? settings.get_sync : settings.get)('user_tags');
  524. var def = '';
  525. if (tmp[user] !== undefined)
  526. def = tmp[user];
  527. var tag = prompt('Tag for ' + user + ' or blank for default:', def);
  528. if (tag === null)
  529. return;
  530. tag = strip_tags(tag);
  531.  
  532. // Ugh.
  533. var tags = { };
  534. for (var u in tmp)
  535. tags[u] = tmp[u];
  536.  
  537. if (tags[user] !== undefined && tag === '')
  538. delete tags[user];
  539. else if (tag !== '')
  540. tags[user] = tag;
  541. settings.set('user_tags', tags);
  542. }});
  543.  
  544. return {
  545. // Register posts to have their badges handled
  546. register: function(p) {
  547. posts = p;
  548. for (var i = 0; i < p.length; i++) {
  549. var post = p[i];
  550. post.saved_badge = post.badge.innerHTML;
  551. tag_prompt(post.badge, post.user);
  552. }
  553. settings.watch('user_tags', update_badges);
  554. },
  555.  
  556. update: function() {
  557. settings.register('user_tags', null, { }, null);
  558. },
  559. };
  560. })();
  561. // Track the last viewed page and post count for threads
  562. var last_viewed = (function() {
  563. var db;
  564. var store = 'thread_data';
  565.  
  566. function GetCmd(args) { this.args = args; }
  567. GetCmd.prototype.mode = 'readonly';
  568. GetCmd.prototype.success = function(e) {
  569. var t = e.target.result;
  570. if (t !== undefined)
  571. this.args.f(this.args.e, t.lvp, t.lpn, t.lp);
  572. };
  573. function SetCmd(args) { this.args = args; }
  574. SetCmd.prototype.mode = 'readwrite';
  575. SetCmd.prototype.success = function(e) {
  576. var t = e.target.result;
  577. if (t === undefined)
  578. t = {id: this.args.id};
  579. t.lvt = this.args.lvt;
  580. t.lvp = this.args.lvp;
  581. if (t.lpn === undefined || t.lpn < this.args.lpn) {
  582. t.lpn = this.args.lpn;
  583. t.lp = this.args.lp;
  584. }
  585. this.os.put(t);
  586. };
  587. GetCmd.prototype.exec = SetCmd.prototype.exec = function() {
  588. this.os = db.transaction(store, this.mode).objectStore(store);
  589. this.os.get(this.args.id).onsuccess = this.success.bind(this);
  590. };
  591.  
  592. // Accumulate commands and fire them once idb is open
  593. var cmds = [];
  594. function run_cmds() {
  595. if (db === undefined || db === null)
  596. return open_db();
  597.  
  598. var cmd;
  599. while ((cmd = cmds.shift()) !== undefined)
  600. cmd.exec();
  601. }
  602.  
  603. // Open idb and maybe remove old threads
  604. function open_db() {
  605. if (db === undefined)
  606. db = null;
  607. else
  608. return;
  609. var req = indexedDB.open('ignore_list', 2);
  610. req.onupgradeneeded = function(e) {
  611. var tmp = e.target.result;
  612. if (!tmp.objectStoreNames.contains(store)) {
  613. var os = tmp.createObjectStore(store, { keyPath: 'id' });
  614. os.createIndex('lvt', 'lvt');
  615. }
  616. };
  617. req.onsuccess = function(e) {
  618. db = e.target.result;
  619. run_cmds();
  620.  
  621. /* Periodically scrub the database (FIXME this is awful & may run in
  622. * multiple tabs simultaneously)
  623. */
  624. settings.register('lv_scrub_time', null, 0, null);
  625. settings.watch('lv_scrub_time', function(n, v) {
  626. var now = Date.now();
  627. if (v + 24 * 60 * 60 * 1000 > now)
  628. return;
  629.  
  630. var tr = db.transaction(store, 'readwrite');
  631. var os = tr.objectStore(store);
  632. os.index('lvt')
  633. .openCursor(IDBKeyRange.upperBound(now - 60 * 24 * 60 * 60 * 1000))
  634. .onsuccess = function(e) {
  635. var c = e.target.result;
  636. if (c) {
  637. os.delete(c.primaryKey);
  638. c.continue();
  639. } else {
  640. settings.set('lv_scrub_time', now);
  641. }
  642. };
  643. });
  644. };
  645. }
  646.  
  647. return {
  648. /* Record a visit to thread_id, on page last_viewed_page, with up to
  649. * last_post_nr (of id last_post_id) visible. Should be called on every
  650. * (thread) pageview.
  651. */
  652. set: function(thread_id, last_viewed_page, last_post_nr, last_post_id) {
  653. cmds.push(new SetCmd({id: thread_id, lvp: last_viewed_page,
  654. lpn: last_post_nr, lp: last_post_id}));
  655. run_cmds();
  656. },
  657.  
  658. // Call func with the view history of thread_id (and pass extra as well)
  659. get: function(thread_id, func, extra) {
  660. cmds.push(new GetCmd({id: thread_id, f: func, e: extra}));
  661. run_cmds();
  662. },
  663.  
  664. update: open_db,
  665. };
  666. })();
  667. // Generic settings menu
  668. var settings_menu = (function() {
  669. function mangle_opts(opts) {
  670. var ret = '';
  671. for (var k in opts)
  672. ret = ret + '<option value="' + k + '">' + opts[k].name + '</option>';
  673. return ret;
  674. }
  675.  
  676. var watch_disable = handlers.generic({ setting: function(n, v) {
  677. this.elm.disabled = v ^ this.extra;
  678. }});
  679.  
  680. var select = handlers.generic({
  681. onchange: function() { settings.set(this.data, this.elm.value); },
  682. setting: function(n, v) { this.elm.value = v; }
  683. });
  684.  
  685. return {
  686. /* Attach a settings menu to elm. Menus is a list of submenus, in the form
  687. * [ { title: 'str' | undefined, vars: [ "name" ] } ]
  688. */
  689. register: function(elm, menus) {
  690. var s = settings.all();
  691. var d = document.createElement('div');
  692. var html = '';
  693. var vs = [];
  694. for (var i = 0; i < menus.length; i++) {
  695. var menu = menus[i];
  696. if (i !== 0)
  697. html += '<hr></hr>';
  698. if (menu.title !== undefined)
  699. html += '<b>' + menu.title + '</b>';
  700. for (var j = 0; j < menu.vars.length; j++) {
  701. var v = menu.vars[j];
  702. vs.push(v);
  703. var sv = s[v];
  704. if (sv.opt !== undefined) {
  705. html += '<div><select style="margin-left: 21px">' +
  706. mangle_opts(sv.opt) + '</select></div>';
  707. } else {
  708. html += '<div><input type="checkbox" id="ilcb_' + v + '"></input>' +
  709. '<label for="ilcb_' + v + '">' + sv.desc + '</label></div>';
  710. }
  711. }
  712. }
  713. d.innerHTML = html;
  714. var e = d.firstElementChild;
  715. var depre = /(!)?(.*)/;
  716. function h(e, v) {
  717. var sv = s[v];
  718. if (sv.opt !== undefined)
  719. select(e, v);
  720. else
  721. handlers.checkbox(e, v);
  722. var d = sv.dep;
  723. if (d) {
  724. var ds = sv.dep.match(depre);
  725. var i = !ds[1];
  726. d = ds[2];
  727. watch_disable(e, d, i);
  728. }
  729. }
  730. while (e !== null) {
  731. if (e.tagName === 'DIV')
  732. h(e.firstElementChild, vs.shift());
  733. e = e.nextElementSibling;
  734. }
  735. elm.appendChild(d);
  736. }
  737. };
  738. })();
  739. // Display a nice menu for managing the ignorelist
  740. var manage_menu = (function() {
  741. var menu_list;
  742. var menu_elms = [];
  743. var template = document.createElement('tr');
  744. template.innerHTML =
  745. '<td></td><td align="right"><a class="itmremove"></a></td>';
  746.  
  747. function create_elm(user) {
  748. var tr = template.cloneNode(true);
  749. tr.firstElementChild.innerHTML = user;
  750. var a = tr.lastElementChild.firstElementChild;
  751. a.href = 'javascript:void(0)';
  752. handlers.ignore_toggle(a, user);
  753. if (this.appendChild)
  754. this.appendChild(tr);
  755. return { user: user.toLowerCase(), elm: tr };
  756. }
  757.  
  758. function lc_comp(a, b) {
  759. return a.toLowerCase().localeCompare(b.toLowerCase());
  760. }
  761.  
  762. function update(user, state) {
  763. if (menu_list === undefined)
  764. return;
  765. if (user instanceof Array) {
  766. var frag = document.createDocumentFragment();
  767. menu_elms = user.sort(lc_comp).map(create_elm, frag);
  768. menu_list.appendChild(frag);
  769. } else {
  770. var lc = user.toLowerCase();
  771. var idx;
  772. if (state) {
  773. for (idx = 0; idx < menu_elms.length; idx++) {
  774. if (menu_elms[idx].user.localeCompare(lc) >= 0)
  775. break;
  776. }
  777. var elm = create_elm.call(0, user);
  778. menu_elms.splice(idx, 0, elm);
  779. if (idx + 1 < menu_elms.length) {
  780. menu_list.insertBefore(menu_elms[idx].elm, menu_elms[idx + 1].elm);
  781. } else {
  782. menu_list.appendChild(menu_elms[idx].elm);
  783. }
  784. } else {
  785. for (idx = 0; idx < menu_elms.length; idx++) {
  786. if (menu_elms[idx].user.localeCompare(lc) >= 0)
  787. break;
  788. }
  789. if (idx < menu_elms.length) {
  790. menu_elms[idx].elm.remove();
  791. menu_elms.splice(idx, 1);
  792. }
  793. }
  794. }
  795. }
  796.  
  797. var addbox_enter = handlers.generic({
  798. onkeydown: function(k) {
  799. if (k.which !== 13)
  800. return true;
  801.  
  802. k.stopPropagation();
  803. k.preventDefault();
  804.  
  805. ignore_list.set(strip_tags(this.elm.value), true);
  806. this.elm.value = '';
  807.  
  808. return false;
  809. }
  810. });
  811.  
  812. var addbox_click = handlers.generic({
  813. onclick: function() {
  814. ignore_list.set(strip_tags(this.data.value), true);
  815. this.data.value = '';
  816. }
  817. });
  818.  
  819.  
  820. return {
  821. // Attach management elements to elm
  822. register: function(elm) {
  823. var tbl = document.createElement('table');
  824. tbl.className = 'itmanage';
  825. tbl.innerHTML = '<tbody><tr><td colspan="2">' +
  826. '<input type="text" placeholder="Add user..." width="auto">' +
  827. '</input> <div class="il_addbtn">Add</div></td></tr></tbody>';
  828. menu_list = tbl.firstElementChild;
  829.  
  830. var td = menu_list.firstElementChild.firstElementChild;
  831. addbox_enter(td.firstElementChild);
  832. addbox_click(td.lastElementChild, td.firstElementChild);
  833.  
  834. ignore_list.watch('late', update);
  835. elm.appendChild(tbl);
  836. },
  837. };
  838. })();
  839. // Where are we?
  840. // In the interest of sanity, ftl.thread is true for all thread-like pages
  841. // (i.e. any page with one or more posts); ftl.threadlist is true for all
  842. // threadlist-like pages (i.e. any page with a list of threads). The
  843. // formatting for all such pages is similar enough to treat them equivalently.
  844. var ftl = {
  845. // Mobile pages
  846. mobile: '^https?://m\\.',
  847. // All forum pages
  848. forums: '^https?://(m|www)\\.fasttech\\.com/forums',
  849. // Thread-like pages (i.e. those with posts)
  850. thread: '^https?://(m|www)\\.fasttech\\.com/forums/[^/]+/t/[0-9]+/.',
  851. // Permalink pages
  852. perma: '^https?://(m|www)\\.fasttech\\.com/forums' +
  853. '/[^/]+/t/[0-9]+/[^/]+\\?[0-9]+$',
  854. // Threadlist-like pages (i.e. those with thread links)
  855. threadlist: '^https?://(m|www)\\.fasttech\\.com/forums' +
  856. '(/[^/]+(/page/[0-9]+)?(/search\\?.*)?)?$',
  857. // Lists of threads for a single author
  858. author: '^https?://(m|www)\\.fasttech\\.com/forums/author/',
  859. // Lists of threads for a single product
  860. product: '^https?://(m|www)\\.fasttech\\.com/p(roducts?)?/',
  861. // New thread page for a product
  862. pnthread: '^https?://(m|www)\\.fasttech\\.com/forums/[0-9]+/post',
  863. // Forum settings page
  864. settings: '^https?://www\\.fasttech\\.com/forums/settings$',
  865. // New arrivals pages
  866. new_arrivals: '^https?://(m|www)\\.fasttech\\.com/category/1/new-products',
  867. // Review pages
  868. reviews: '^https?://(m|www)\\.fasttech\\.com/reviews',
  869. // Product search
  870. prsearch: '^https?://(m|www)\\.fasttech\\.com/search',
  871. // Product category
  872. prcategory: '^https?://(m|www)\\.fasttech\\.com/c(ategory)?(/|$)',
  873. };
  874. for (var k in ftl)
  875. ftl[k] = new RegExp(ftl[k]).test(location.href);
  876. // ftl.editor matches all pages with a post editor
  877. ftl.editor = ftl.thread ||
  878. (ftl.threadlist && !ftl.settings && !ftl.author) ||
  879. ftl.pnthread;
  880. // ftl.threadlist matches all pages listing threads
  881. ftl.threadlist = ftl.threadlist || ftl.author || ftl.product || ftl.settings;
  882. // ftl.forums matches all pages with forum elements (threads or posts)
  883. ftl.forums = ftl.forums || ftl.product;
  884. // ftl.reviews matches all pages with reviews
  885. ftl.reviews = ftl.reviews || ftl.product;
  886.  
  887. if (ftl.thread || ftl.settings) {
  888. // Menu configuration
  889. var full_menus = [
  890. { title: 'Browsing', vars: ['use_theme', 'theme_name', 'track_viewed',
  891. 'track_posts', 'always_unread', 'hide_threads'] },
  892. { title: 'Posting', vars: ['quote_quotes', 'quote_imgs'] },
  893. { title: 'Thread Menu', vars: ['inthread_menu', 'inthread_manage'] },
  894. { title: 'Post Hiding', vars: ['placeholders', 'hide_ignored',
  895. 'hide_quotes', 'hide_deleted'] },
  896. ];
  897. var thread_menu;
  898. if (true) {
  899. thread_menu = [
  900. { title: 'General', vars: ['use_theme', 'quote_quotes', 'quote_imgs'] },
  901. { title: 'Post Hiding',
  902. vars: ['hide_ignored', 'hide_quotes', 'hide_deleted'] },
  903. ];
  904. } else {
  905. thread_menu = full_menus;
  906. }
  907.  
  908. settings.register('inthread_menu', 'Show menu in thread toolbar', true, null);
  909. settings.register('inthread_manage', 'Manage ignored users from the menu',
  910. false, 'inthread_menu');
  911. settings.register('placeholders', 'Show placeholders for hidden posts', true,
  912. null);
  913. settings.register('hide_quotes', 'Hide posts quoting ignored users', false,
  914. 'hide_ignored');
  915. settings.register('hide_deleted', 'Hide deleted posts', true, null);
  916. settings.register('quote_quotes', 'Include quotes in quotes', true, null);
  917. settings.register('quote_imgs', 'Include images in quotes', true, null);
  918. }
  919. if (ftl.threadlist) {
  920. settings.register('track_viewed', 'Link to last viewed page', false, null);
  921. settings.register('track_posts', 'Link to unread posts', true, null);
  922. settings.register('always_unread', 'Always show unread posts button', false,
  923. 'track_posts');
  924. settings.register('hide_threads', 'Hide threads by ignored users', false,
  925. null);
  926. }
  927. if (ftl.editor) {
  928. settings.register('agree_tou', null, false, null);
  929. }
  930.  
  931. block.block('ready');
  932. if (ftl.forums) {
  933. var ignore_list = create_ignore_list('ignorelist');
  934. last_viewed.update();
  935. if (ftl.thread || ftl.reviews)
  936. user_tags.update();
  937. block.block('late');
  938. block.watch('ready', function() {
  939. setTimeout(function() { block.unblock('late'); }, 0);
  940. });
  941. } else if (ftl.new_arrivals) {
  942. var ignore_categories = create_ignore_list('ignored_categories');
  943. } else if (ftl.reviews) {
  944. user_tags.update();
  945. }
  946.  
  947. settings.update();
  948.  
  949. if (document.readyState === 'loading') {
  950. document.addEventListener('readystatechange', function() {
  951. if (document.readyState === 'interactive') block.unblock('ready');
  952. });
  953. } else {
  954. block.unblock('ready');
  955. }
  956.  
  957. /* Fix up display of certain post elements:
  958. * - Fix align tags (which should have been 100% divs to begin with)
  959. * - Avoid breaking SKU links across lines
  960. */
  961. var style = document.createElement('style');
  962. style.innerHTML =
  963. '.PostContent span[style*="text-align:"] {display:block;width:100%}\n' +
  964. 'a.SKUAutoLink {display: inline-block}\n' +
  965. '.PopoutPanel>ul {margin: -5px -20px 0px -50px!important}\n' +
  966. '.PopoutPanel>p {margin: 0px!important}\n' +
  967. '.Badges {cursor:pointer}\n' +
  968. '.itmanage tbody tr td {padding:0px}\n' +
  969. '.itmanage .itmremove {padding-left: 16px; height: 16px; background: ' +
  970. 'url(https://www.fasttech.com/images/minus-button.png) no-repeat}';
  971. document.head.appendChild(style);
  972.  
  973. // RE to pull a thread id from a URL
  974. if (ftl.forums)
  975. var thread_id_re = new RegExp('/forums/[^/]+/t/([0-9]+)/[^?]*$');
  976.  
  977. if (ftl.thread) {
  978. // Add a filter for posts quoting ignored users
  979. post_filter.filter((function() {
  980. var quote_res = [];
  981. function new_qre(user) {
  982. quote_res.push(
  983. new RegExp('\\b' + user + ' wrote(</a>)?:?[\\r\\n]*<br>', 'i'));
  984. }
  985.  
  986. function update(user, state, idx) {
  987. if (user instanceof Array) {
  988. quote_res = [];
  989. for (var i = 0; i < user.length; i++)
  990. new_qre(user[i]);
  991. } else {
  992. if (state)
  993. new_qre(user);
  994. else
  995. quote_res.splice(idx, 1);
  996. }
  997. }
  998. ignore_list.watch('early', update);
  999. settings.watch('hide_quotes', post_filter.update);
  1000.  
  1001. return function(post) {
  1002. if (settings.get('hide_ignored') && settings.get('hide_quotes')) {
  1003. for (var i = 0; i < quote_res.length; i++) {
  1004. if (quote_res[i].test(post.content_elm.innerHTML))
  1005. return true;
  1006. }
  1007. }
  1008. };
  1009. })());
  1010.  
  1011. // Add a filter for posts that the admins have deleted
  1012. post_filter.filter((function() {
  1013. var deleted_re = /post deleted/i;
  1014. var edited_re = /^[ \r\n]*$/;
  1015. settings.watch('hide_deleted', post_filter.update);
  1016.  
  1017. return function(post) {
  1018. if (settings.get('hide_deleted')) {
  1019. var wb = post.content_elm.getElementsByClassName('WarningBox')[0];
  1020. if ((wb !== undefined && deleted_re.test(wb.innerHTML)) ||
  1021. edited_re.test(post.content_elm.innerHTML))
  1022. return true;
  1023. }
  1024. };
  1025. })());
  1026.  
  1027. // Get all post bodies, desktop and mobile
  1028. var posts;
  1029. block.watch('ready', function() {
  1030. posts = [];
  1031. var elms = document.getElementsByClassName('PostContent');
  1032. for (var i = 0; i < elms.length; i++) {
  1033. var pc = elms[i];
  1034. var user = pc.getAttribute('data-username');
  1035. if (user !== null)
  1036. posts.push({ user: user, content_elm: pc });
  1037. }
  1038. });
  1039.  
  1040. // Create a function to create placeholders as needed
  1041. var gen_watch_filter = function(ph_func) {
  1042. return function(post, state) {
  1043. var placeholders = settings.get('placeholders');
  1044. if (state && placeholders) {
  1045. if (!post.placeholder)
  1046. post.placeholder = ph_func(post);
  1047. post.placeholder.style.display = '';
  1048. } else {
  1049. if (post.placeholder !== undefined)
  1050. post.placeholder.style.display = 'none';
  1051. }
  1052. }
  1053. };
  1054.  
  1055. // Get this thread's ID
  1056. var thread_id = location.href.match(thread_id_re);
  1057. if (thread_id !== null && thread_id[1] !== undefined)
  1058. thread_id = parseInt(thread_id[1]);
  1059. else
  1060. thread_id = undefined;
  1061. }
  1062.  
  1063. // Get the current and last pages from a pager element, desktop and mobile
  1064. function parse_pager(elm) {
  1065. var cur, last, sel;
  1066. if (elm === undefined || elm === null) {
  1067. cur = last = 1;
  1068. } else if (!ftl.mobile) {
  1069. sel = elm.getElementsByClassName('PageLink_Selected')[0];
  1070. if (sel !== undefined)
  1071. cur = parseInt(sel.innerHTML);
  1072. last = parseInt(elm.lastElementChild.previousElementSibling.innerHTML);
  1073. } else {
  1074. sel = elm.getElementsByClassName('active')[0];
  1075. if (sel !== undefined)
  1076. cur = parseInt(sel.firstElementChild.innerHTML);
  1077. last = parseInt(elm.lastElementChild.firstElementChild.innerHTML);
  1078. }
  1079. return {
  1080. cur: cur,
  1081. last: last
  1082. };
  1083. }
  1084.  
  1085. if (!ftl.mobile) {
  1086. /* Jump-to-page boxes are broken site-wide. Fix them.
  1087. * The existing JumpToPage box could be fixed by changing "function (e)" to
  1088. * "function (event)" in the event listener, but instead, I'm doing all this.
  1089. */
  1090. var fix_jtp = function() {
  1091. var jtps = document.getElementsByClassName('JumpToBox');
  1092. if (jtps.length === 0)
  1093. return;
  1094.  
  1095. var lastpg = parse_pager(jtps[0].parentElement).last;
  1096. function handle_kp(k) {
  1097. if (k.which === 13) {
  1098. k.preventDefault();
  1099. k.stopPropagation();
  1100. var pg = parseInt(this.value);
  1101. if (pg > 0 && pg <= lastpg)
  1102. location.href = base[1] + pg + base[2];
  1103. }
  1104. return false;
  1105. }
  1106.  
  1107. for (var i = 0; i < jtps.length; i++) {
  1108. var jtpbox = jtps[i];
  1109. var as = jtpbox.parentElement.getElementsByTagName('a');
  1110. if (as[0] !== undefined) {
  1111. var base = as[0].href.match(new RegExp('(.*/)[0-9]+($|[?#/].*)'));
  1112. if (base !== null)
  1113. jtpbox.addEventListener('keypress', handle_kp);
  1114. }
  1115. }
  1116. };
  1117. block.watch(ftl.forums ? 'late' : 'ready', fix_jtp);
  1118. }
  1119.  
  1120. if (ftl.threadlist) {
  1121. // Translate a post number into a page number, handling FT's off-by-one quirk
  1122. var postnr_to_pagenr = function(post, maxpage) {
  1123. return Math.floor(Math.min(maxpage, post / 10 + 1));
  1124. };
  1125.  
  1126. // Create a link to a specific page of a thread
  1127. var gen_page_link = function(base, page, hash) {
  1128. var ret = base;
  1129. if (page !== 1)
  1130. ret += '/' + page;
  1131. if (hash)
  1132. ret += '#' + hash;
  1133. return ret;
  1134. };
  1135. }
  1136.  
  1137. if (!ftl.mobile) {
  1138. // Re-implement FT's PopoutMenu, only nicer
  1139. var PM2 = (function() {
  1140. var active, timeout;
  1141.  
  1142. function show() {
  1143. var p = active.par;
  1144. var c = active.ch;
  1145. p.className += ' focused';
  1146. c.style.left = c.style.top = '0px';
  1147. c.style.display = 'inline';
  1148. var ppb = p.parentElement.getBoundingClientRect();
  1149. var pb = p.getBoundingClientRect();
  1150. var cb = c.getBoundingClientRect();
  1151. c.style.left = Math.min(pb.left - cb.left,
  1152. document.body.clientWidth - cb.width) + 'px';
  1153. c.style.top = (ppb.bottom - cb.top) + 'px';
  1154. }
  1155.  
  1156. function hide() {
  1157. if (active === undefined)
  1158. return;
  1159. active.par.className = active.cname;
  1160. active.ch.style.display = 'none';
  1161. active = undefined;
  1162. }
  1163.  
  1164. function min(pm) {
  1165. clearTimeout(timeout);
  1166. if (pm !== active)
  1167. hide();
  1168. active = pm;
  1169. show();
  1170. }
  1171.  
  1172. function mout() { timeout = setTimeout(hide, 300); }
  1173.  
  1174. function add_listeners(elem, pm) {
  1175. elem.addEventListener('mouseover', min.bind(null, pm));
  1176. elem.addEventListener('mouseout', mout);
  1177. }
  1178.  
  1179. /* Hack: we can't remove anonymous event listeners, so duplicate elements.
  1180. * We hide the existing element to serve as a decoy for the original
  1181. * PopoutMenu in case it hasn't run yet, then steal all its children.
  1182. */
  1183. function dupe(id) {
  1184. var elm = document.getElementById(id);
  1185. var nelm = elm.cloneNode(false);
  1186. elm.style.display = 'none';
  1187. while (elm.childNodes[0])
  1188. nelm.appendChild(elm.childNodes[0]);
  1189. if (elm.nextElementSibling)
  1190. elm.parentElement.insertBefore(nelm, elm.nextElementSibling);
  1191. else
  1192. elm.parentElement.appendChild(nelm);
  1193. return nelm;
  1194. }
  1195.  
  1196. var def = {dupe: true};
  1197. return function(pm) {
  1198. for (var k in def) {
  1199. if (!(k in pm))
  1200. pm[k] = def[k];
  1201. }
  1202. try {
  1203. if (pm.dupe) {
  1204. pm.ch = dupe(pm.ch || (pm.par.replace('Button', '') + 'Popout'));
  1205. pm.par = dupe(pm.par);
  1206. } else {
  1207. pm.ch = document.getElementById(
  1208. pm.ch || (pm.par.replace('Button', '') + 'Popout'));
  1209. pm.par = document.getElementById(pm.par);
  1210. }
  1211. pm.cname = pm.par.className;
  1212. pm.ch.style.padding = '20px';
  1213. add_listeners(pm.par, pm);
  1214. add_listeners(pm.ch, pm);
  1215. } catch (e) {}
  1216. };
  1217. })();
  1218.  
  1219. block.watch('ready', function() {
  1220. ['Cart', 'Support', 'Community', 'Account']
  1221. .forEach(function(n) { PM2({par: n + 'Button'}); });
  1222. if (ftl.thread) {
  1223. ['RateThread', 'ForumTools']
  1224. .forEach(function(n) { PM2({par: n}); });
  1225. }
  1226. });
  1227. }
  1228. styles.register('nightmode', undefined,
  1229. '#SearchKeywords,#images ol,#searchBarCategory,button.close{background:0 ' +
  1230. '0!important}#images ol{border:0!important}.btn.active,li.active>a{backgr' +
  1231. 'ound:#0d7bad!important}.FormButton.green{background:#20944b!important}#P' +
  1232. 'roductDetails,#RightPanel,#categoryButton,#container .list-group li a,#f' +
  1233. 'ooter,#searchBarOptionsCell,#searchBoxCell,#searchButtonCell,.AFEntry,.A' +
  1234. 'ttachments,.BGShadow,.BGShadowLight,.OrdersShipmentHeading,.PageNavigati' +
  1235. 'onalPanel,.ProductBackdrop,.ProductFilters,.ProductsGrid,.TicketResponse' +
  1236. ',.TicketResponseFrame,.alert-info,.jumbotron,.list-group li,.mbbc-popups' +
  1237. ' li,.mbbc-popups ul,.ui-dialog,body,td.Pros,ul.nav a{background:#222!imp' +
  1238. 'ortant}.FormButton.blue{background:#3182b9!important}#AccountButton.focu' +
  1239. 'sed,#LeftPanel,#UpdateWarningMsg,#container li a,#container li.active a.' +
  1240. 'dropdown-toggle,#container ol,#navbar>tbody>tr:first-child,.CardShadow,.' +
  1241. 'ControlArrows,.FilterEntry:hover,.FormButton.white,.ForumHeader,.ForumLi' +
  1242. 'nk:hover,.GridItem,.PopoutPanel,.ProductGridItem,.ProductPanelBackdrop,.' +
  1243. 'ReviewContent,.Steps>div,.btn,.dropdown-menu,.dropit-submenu>li,.mbbc-po' +
  1244. 'pups li:hover,.navbarsearch,.panel-body,.panel-heading,.panel-title,.tab' +
  1245. 'le,.table td,.tabs-min,button,div.well,input:not([valbbcode]),select,tex' +
  1246. 'tarea,ul.dropdown-menu>li>a{background:#333!important}.FilterSelected,.F' +
  1247. 'orumLink.Selected,.PageLink:hover,.StaticPageLink_Selected,.divider,.dro' +
  1248. 'p-hover>a,.dropit-submenu,.subCategories li:hover,hr{background:#444!imp' +
  1249. 'ortant}.FormButton.purple{background:#6a3b7f!important}.FormButton.red{b' +
  1250. 'ackground:#9e1531!important}.FormButton.orange{background:#c4571d!import' +
  1251. 'ant}.topLevelCategories li.focused{background-image:url(data:image/png;b' +
  1252. 'ase64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAZklEQVR4AWPwySwkG1' +
  1253. 'NPs2TjRjuyNUs0brwl0bRxj3jzei2yNEs2bfwPwkBD+sTb1wuQrhlhwCvJ5o25ZGhGYJA4KD' +
  1254. 'zI0YwwpGF9LMmaQYEIsplkP0s0b4wnO7TpE88gTXRN22RjAIeVNYXGl+mUAAAAAElFTkSuQm' +
  1255. 'CC)!important;background-position:right 2px!important;background-repeat:' +
  1256. 'no-repeat!important}.mbbc-buttons li{background-image:url(data:image/png' +
  1257. ';base64,iVBORw0KGgoAAAANSUhEUgAAAGwAAACVCAMAAACKJe8uAAAAjVBMVEVMaXG0UlJg' +
  1258. 'YmS+kI/MzMzMzMzMzMzMzMzAxLuoqKiamppWV1cuLi7MzMzMzMzAx8qNjo5FRUaoRkZ8fH1N' +
  1259. 'Tk/MzMzMzMyUPz55OjfMzMzMzMzMzMzMzMzMzMxWeJzJbgbGpxKCoqY8YIC5uqhzqsmwlV99' +
  1260. 'pIB1lTxlghlvfMlxYbKnRy1sk7skfGsNbDRy3rXhAAAAHnRSTlMA/////42hdf//////s8L/' +
  1261. '///////W6///NiMRW0Sx+uQWAAAC5klEQVR4AezSRZLAMAxEUY3SQTkM8v0vOhxG7fNdXr9A' +
  1262. '09vbqo9VZIuZTVaARcGshVEUhfGNlaSZWDDAwWFsxgggylFcWSWzVGLD0lOMgKv3YqLagjVA' +
  1263. '61rnHH4OGguWsnxjnQn7ZY6xEP0FxmXdjZYFcz93i/XDzS+rK5NFzXqNTz8jM39bTkwWabNK' +
  1264. 'N1gBnGz+2yJr2gD4YXCI5cChxVzVzo6NzDE2IDq0SMbNm7HfgwBYYBHQR31xsnkiqToz5hvF' +
  1265. 'yCkaTw/ijEVau0VeFQr8Xqg+wkiqcfNGzOsi/wyjrvtip761hllBKAzvArGSTtcS8P5v8/x+' +
  1266. 'OceRk3iaPd07+QetXXsGKaX0bzQLl4kolRRhWBBnVIRRmghTB+III4yRI4yTIUzriKKV+kKk' +
  1267. 'lFJKiZ7ABZyZF6CFi+JSzlyX45lCjm3Y+1eGH6jNsMaLmJNiE1yr745/JXbBM+tigAVdWaWx' +
  1268. 'Sz+4MvyADxJFyJWtPxpRnW/G+MNnhu/ixsyNRsFTk/whdqVdQSU8Jw27tS5+Zl13seE561IL' +
  1269. 'Oy4mUpzF8JIuu/7faLCl+BvJI0dKKaUvMbe7xXHOhm3tPax2u13BcZXmw56m1LAZMY5jcmyF' +
  1270. 'Fr7F1jOGLxikD/sti55Z+AIivVuLiPVeFHa7h28jbXy3DWcJbQ971twNuduGw6aMog+bUkop' +
  1271. 'fYPI7U43Xcdj2LTVWbqExHQ4UKmExDoDKDQjYnUYgDYQERsMwAZHxBY5ACaPiFWagHeyiJhT' +
  1272. 'sVpF1HHYEKDTcPRR8V8jzw4OG3IPKaX/vb/aqQcchmIAAMPzmjzGC2rz/rebbfvvAb7637eX' +
  1273. 'rdfYCq13DwxjnBFCMkrpLtYBoJrVYQ/HOCFUTKuegGEphNL0XtjRMwO1ENQYfS/MWps55zLv' +
  1274. '/S5WjTdQG2PUV2JCmSedmZo0oEqBu2AhhCzGmKWUdjFu9CzDn4EtegIGxj/IeIzrPP7MqlVj' +
  1275. '7LEhtuoWbAQUelFEFIeMmgAAAABJRU5ErkJggg==)!important;border-radius:5px!im' +
  1276. 'portant}.HourGlass{background:inherit!important}.Stars{background:url(da' +
  1277. 'ta:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAXVBM' +
  1278. 'VEVMaXH/rVz/s2f/jCv/uGz/vXD+iSf+jSz+kjD+xH3+iiX+jCr+s2P/uG/+wXf+iif+m0T+' +
  1279. 'n0f+pE7+p1L+v3X+wHj+wn3/kjP/mD3/oEj/p1P/tGX/vnT/iCX/xX3uTskzAAAAF3RSTlMA' +
  1280. '/grK/i3p1yHXJX7WA3aZbPXD7o2sP+NiEhsAAABkSURBVHgBZcvFAcMwEEXBL2aFzXb/ZUYY' +
  1281. 'fLdZQI0GfHaPnxo2Lt6iZls9bRDRbHwl17NGgjmOrGWeRmWBx1aVeEJ69GtSXrLyLkjdSY3S' +
  1282. 'rWhUqF3mKWmXjQmM7Z1BOQ1Yx/DXE5S8Bqiv9nzRAAAAAElFTkSuQmCC)!important}.Gra' +
  1283. 'yStars{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4A' +
  1284. 'AAAOCAYAAAAfSC3RAAAA4UlEQVR4AWLwySzEinfu3MkGwrjkcWo8efJk76lTp8pI0njmzBn7' +
  1285. 'Y8eO/T948OB/IG2AXyPCJl5AZ3RthjAARAF4F0omZBWquKfBrcPdOmyEMELk8XFxL+L5T0+n' +
  1286. '04fgbDa7p5UcBk2Utt1u74fDwSE4nU6dwWCwkGW5JYpiw4coBRnwM444wtHtdh3TNB1FURye' +
  1287. '558I8NNeJpNAGgpDjuPaMH5fKBEgDwmCsKd+Iz0SSEEo0UKJqVMdjUZfAmFEvWWuIwzCiGXZ' +
  1288. 'fEiA+sEB9IdWKkTTiBrfF+7xDkHisNbhAqOEltIAVAofAAAAAElFTkSuQmCC)!important}' +
  1289. '.FilterEntry.FilterCancellable{background:url(data:image/png;base64,iVBO' +
  1290. 'Rw0KGgoAAAANSUhEUgAAAAwAAAALCAYAAABLcGxfAAAASklEQVR4AWPwySxkuCUk9J8QBqkD' +
  1291. 'YbBiYjGKBigAs3GJMTAwNDBgUYDOxtSAoQmhmCwNlDmJTE8TxggNiIgjrBimAYxBHEIYpA4A' +
  1292. 'F8kQ7Ga9snUAAAAASUVORK5CYII=) 10px 3px no-repeat,#444!important;border-c' +
  1293. 'olor:#0d7bad!important;box-shadow:0 2px 2px #111!important}input[src="/i' +
  1294. 'mages/errorreport.png"]{background:url(data:image/png;base64,iVBORw0KGgo' +
  1295. 'AAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAMFBMVEX////////////////////////////' +
  1296. '///////////////////////////////////9Or7hAAAAAEHRSTlMBCBIcJS45RlVkcYOVqbz' +
  1297. 'NHir32QAAAGNJREFUeAEUTAkBBEEIUhMIDeAazPbvdjgvn0xVAXkCGocCwKcNaH4/9Sn08ym' +
  1298. 'Qae00DUBRAtbGIHJUDc1Y1DRuKRY25Y3/gOa8BwKQFe/ugcAHpnfv7p49e/bcPab/Z8HgPgC' +
  1299. 'LAx2nigq2jAAAAABJRU5ErkJggg==)!important}.CellMode .GridItemPrice{backgr' +
  1300. 'ound:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHUAAAAeCAYAAAASN' +
  1301. '1ElAAAAi0lEQVR4Ae3aSRXDIABAwZiIjgrAQkV0X46VUT1RFAOV0M3Gf3MYBR8IEKbXfvMhZ' +
  1302. 'RG1Z4jasm7vj6kVlUswqlnaisqlFZX1Z25F5fmP2YrKHIxqlraiMovasvwjtqIyglHN0lZUR' +
  1303. 'iAqolp+sVHqcKTB5QOuCXGhj19vfpLjOQsenuGJKB5zM0TtWb7rxpkaVlv9JQAAAABJRU5Er' +
  1304. 'kJggg==) no-repeat!important;width:134px!important}.ForumHeader input[ty' +
  1305. 'pe=image]{border:0!important;vertical-align:text-top!important}#AddedToC' +
  1306. 'artPopupPanel,#container ol,.CardShadow,.ForumThread,.GridItem,.ProductG' +
  1307. 'ridItem,.ProductPanelBackdrop,.TicketResponseFrame,.dropit-submenu,.mbbc' +
  1308. '-buttons li,.panel-body,.panel-heading,td.Pros{border:1px solid #444!imp' +
  1309. 'ortant}#searchBarOptionsCell,#searchBoxCell,#searchButtonCell,.ForumThre' +
  1310. 'ad tr td,.navbarsearch{border-bottom:1px solid #444!important}#Ignorelis' +
  1311. 'tPopout tr td,.PostFlags ul li{border-bottom:none!important}.FilterEntry' +
  1312. ':hover,.FilterSelected,.StaticPageLink_Selected{border-color:#0d7bad!imp' +
  1313. 'ortant}#categoryButton,#container li a,#container ul,.ControlArrows,.For' +
  1314. 'umQuote,.ForumsNavigation,.PageLink,.PageNavigationalPanel,.PostFlags,.S' +
  1315. 'KUAutoLink,.Signature,.TightInformationBox,.list-group li,.mbbc-popups u' +
  1316. 'l,body .FormButton[class],div,hr,td{border-color:#444!important}.btn,but' +
  1317. 'ton,input:not([valbbcode]),select,textarea{border-color:#444!important;b' +
  1318. 'order-style:solid!important}#searchBarOptionsCell{border-left:1px solid ' +
  1319. '#444!important}.ForumQuote{border-left:5px solid #0d7bad!important}.drop' +
  1320. 'it-submenu>li,blockquote{border:none!important}#searchButtonCell{border-' +
  1321. 'right:1px solid #444!important}.ForumLink.Selected,.ForumLink:hover{bord' +
  1322. 'er-right:3px solid #0d7bad!important}#searchBarOptionsCell,#searchBoxCel' +
  1323. 'l,#searchButtonCell{border-top:1px solid #444!important}#LeftPanel,.Popo' +
  1324. 'utPanel{box-shadow:0 15px 30px #111!important}.ProductGridItem{box-shado' +
  1325. 'w:0 3px 3px #111!important}.BGShadow,.BGShadowLight,.CardShadow,.FilterS' +
  1326. 'elected,.ForumLink.Selected,.GridItem,.StaticPageLink_Selected,.subCateg' +
  1327. 'ories li:hover{box-shadow:none!important}.SysMsgBanner{color:#000!import' +
  1328. 'ant}#AccountButton a{color:#0d7bad!important}#StaticPageContent [style],' +
  1329. '#categoryButton a,#content_ProductName,#footer,.AFTitle,.ControlArrows a' +
  1330. ',.FieldLabel,.FlagValue,.Gray,.GrayText,.GridItemDesc>div,.GridItemModel' +
  1331. 's,.Name>a,.NewList,.PageLink,.Pager,.btn,.dropit a,.navmenubutton a,.pan' +
  1332. 'el-default>.panel-heading,.panel-title,.subCategories a,.topLevelCategor' +
  1333. 'ies a,a.SortOrder,body,button,input:not([valbbcode]),select,textarea,ul.' +
  1334. 'dropdown-menu>li>a{color:#bbb!important}.mbbc-popups li{color:#ccc!impor' +
  1335. 'tant}.btn.active,body .FormButton[class],ul.dropdown-menu>li>a:hover{col' +
  1336. 'or:#fff!important}.GridItemPackSize{display:inline!important;float:none!' +
  1337. 'important;margin:0 0 0 -22px!important;vertical-align:sub!important}#DMC' +
  1338. 'A,.logo,.navseals,a[href$="/faq/money-back-guarantee"]>img{display:none!' +
  1339. 'important}#categoryButton{margin:0 20px!important}#footer,.PageContentPa' +
  1340. 'nel>div:last-child[style]{margin:0!important}#CartPopupClose{margin:-10p' +
  1341. 'x -20px!important}.mbbc-row1{margin-bottom:1px!important}.ForumLink:hove' +
  1342. 'r{margin-right:-3px!important}#Tab{margin-top:10px!important}#AddedToCar' +
  1343. 'tPopupPanel{padding:0 10px!important;position:fixed!important;right:40px' +
  1344. '!important;top:40px!important}.menubuttons{padding:0 20px!important}#Ans' +
  1345. 'wer{padding:0!important}#searchbar{padding-right:20px!important}#AddedTo' +
  1346. 'CartPopupPanel,#categoryButton{width:auto!important}');
  1347.  
  1348. styles.register('darkft', undefined,
  1349. '.FieldFlag,.Pager .PageLink{font-size:9pt!important}.Pager .PageLink:hov' +
  1350. 'er{background:#111!important;color:#fefefe!important}.FieldFlag{backgrou' +
  1351. 'nd:#1980b0!important;font-weight:400!important;padding:3px 5px!important' +
  1352. '}.ForumLink.Selected{background:#222!important;border-right:3px solid #6' +
  1353. '66!important}.ForumHeader,.ForumThread .ThreadCommandBar,.logo,body,div#' +
  1354. 'contents{background:#333!important}.logo{visibility:hidden!important}.Fo' +
  1355. 'rumThread tr td{background:#363636!important}#AccountButton.focused,#Acc' +
  1356. 'ountPopout,#AddedToCartPopupPanel,#CartPopout,#CartPopupPanel,#Community' +
  1357. 'Popout,#LeftPanel,#SupportPopout,#categoryButton,#categoryPopout,#navbar' +
  1358. ',#searchBarOptionsButton,.CellMode .ProductGridItem,.FilterEntry:hover,.' +
  1359. 'FormButton.white,.FormButton.white:hover,.LargeFilterEntry:hover,.LineMo' +
  1360. 'de .ProductGridItem,.ModalDialog,.OrdersShipmentHeading,.PageNavigationP' +
  1361. 'anel,.ProductBackdrop,.ProductFilters,.ProductFilters .FilterCancellable' +
  1362. ',.ProductFilters .FilterSelected,.ProductPanelBackdrop,.mbbc .mbbc-edito' +
  1363. 'r,div#footer,hr,tr{background:#444!important}.HourGlass{background:#444f' +
  1364. 'e5!important}.Cons .FieldFlag,.Pros .FieldFlag{background:#4c9ed9!import' +
  1365. 'ant}#navbar .focused,.BGShadow,.BGShadowLight,.PopoutPanel,.Steps .focus' +
  1366. 'ed{background:#666!important}.PopoutPanel{border-radius:0!important;box-' +
  1367. 'shadow:0 5px 5px #888!important}#searchBarCategory{background:#777!impor' +
  1368. 'tant}.Pager .ControlArrows{background:#888!important;font-weight:400!imp' +
  1369. 'ortant}.Pager .PageLink_Selected{background:#999!important}#ForumPopout,' +
  1370. '#ForumToolsPopout,#RateThreadPopout{background:#fff!important}#CartButto' +
  1371. 'n,#CommunityButton,#SupportButton,#searchBarOptionsCell,#searchButtonCel' +
  1372. 'l,.GridViewIcon,.GroupViewIcon,.ProductGridMenu,.searchbar_focus #search' +
  1373. 'BarOptionsCell,.searchbar_focus #searchButtonCell{background-image:url(d' +
  1374. 'ata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAJYBAMAAADh0yLyAAAAJ1B' +
  1375. 'MVEVMaXFqbG11dXX9/f3l5eXd3eBadIdWbnlWc4RYieRmZ2dpaWlsbW2OPQlLAAAADXRSTlM' +
  1376. 'A////////////r0YgYtO1qQAAAq9JREFUeAHt2sd160YYx9FxKgBcekc24ADsHTizVwBakEr' +
  1377. 'g6+CpgBdLcHYJLsA9KTmnZ0PQof+U7j0q4Hc+fMM0KvcAALpI9xsocL2K+/t94GoT6KACNyZ' +
  1378. '4oIF20AQF2kGBdtAEBdpBgXbQBAXaQYF20AQF2kGBdtAEI5UMAAAA4V9oBC43938PV/sO3My' +
  1379. '1FihQoECB+yBQoECBAgUKFChQoECBAgUKFChQoEA3TcsBAAAA9CXbUEuq+pOhpNoO/fVfTZ5' +
  1380. 'ga3Wqn6Y/4hLr3ToM4YFj9gRLHdIDb5VwyTuYPsFh4SExwXeGeh35YQEAAACAf/h1K/8aogq' +
  1381. '8q7rt+74lB/bXhvjAfkwPrAIXHZKaHNj6oUYHXsf1/ZAcONRtn/5O0o/ZgW3rvTjx4xYAAAA' +
  1382. 'ArsIEtjrWlj3BbfojHrbxOziEBw7pE0x/xKfN6+ACday1tpY8wTF7gq2168ZxfHw76CoMAAA' +
  1383. 'AAM4ild+c7xL9PrBLdEiBT1ebQA8hUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChSYftN0cRE' +
  1384. 'ZmAEAAOBstrJfu7melP3q5hL4J5u54gN3AgUKFChQoECBAgUKFChQoECBAgUKFChQoMCMwN1' +
  1385. 'cjzgQAABgmt6bjqZJoEA8YoF4xHsgMA0AADD9ROBs2EHsoEDs4OEDAADaNH1akm1bbSXZONa' +
  1386. 'jo/CvnUfvlUUcklpryz4ktT2PPiRTe/08+pCMS3/68E7yXvg7yadH4e8kU0l/Jyl3BAAAfPm' +
  1387. 'T70uqzU/eTw9c/5Ae+P0jDRQoUKDArutWJrgwcP3QJ+iQdE5x+ATtoAl6oRYoUOAeCBRYUnU' +
  1388. '/+aqk+vEnBQAAF9qRXGgLFChQoMBlXt9YCbSD/5+zGw7Jwgme28EFuht20Au1QIECBQp0oe1' +
  1389. 'COx0AAFf2ljch8gqLTAAAAABJRU5ErkJggg==)!important}#navbar tr.navbarsearch' +
  1390. ',.navsealsbg,body.stripe{background-image:url(data:image/png;base64,iVBO' +
  1391. 'Rw0KGgoAAAANSUhEUgAAAGQAAABGAQMAAAAASKMqAAAAA1BMVE2.5EQ1TRdOAAAAEElEQVR4' +
  1392. 'AWMYVmAUjIJRAAAD1AABVz/btAAAAABJRU5ErkJggg==)!important}.searchbar_focus' +
  1393. ' #searchBoxCell{background-image:url(data:image/png;base64,iVBORw0KGgoAA' +
  1394. 'AANSUhEUgAAAD0AAAAtBAMAAADvmn5BAAAAD1BMVEWYmJmzs7PBwcHGxsagoaK4fiRWAAAAI' +
  1395. 'ElEQVR4AWNiwA+YBPECgcGuf6DkR+VH5UflR+UpLd8AJUsIkz10gdcAAAAASUVORK5CYII=)' +
  1396. '!important}#searchBoxCell{background-image:url(data:image/png;base64,iVB' +
  1397. 'ORw0KGgoAAAANSUhEUgAAAD0AAAAtAQMAAAAnevExAAAAA1BMVEV3d3dY20ihAAAADUlEQVR' +
  1398. '4AWMYfGAUAAABlQABpWSZFgAAAABJRU5ErkJggg==)!important}.SysMsgBanner{borde' +
  1399. 'r:1px dotted #444!important}#DMCA div.dm-1,#DMCA div.dm-2,#LeftPanel,#ca' +
  1400. 'tegoryButton,#searchBarOptionsButton,.BorderedPanel,.FormButton,.ForumTh' +
  1401. 'read,.ForumThread .Signature,.InformationBox,.Pager .ControlArrows,.Page' +
  1402. 'r .PageLink,.Pager .PageLink_Selected,.ProductThumbnails,.Reviews .Cons,' +
  1403. '.SKUAutoLink,.SingleLineTextBox,.TightInformationBox,.VerticalTextArea,.' +
  1404. 'WarningBox{border:1px solid #444!important}.ForumQuote{border:1px solid ' +
  1405. '#555!important;border-left:5px solid #666!important}.mbbc .mbbc-editor{b' +
  1406. 'order:none!important}td{border-bottom:0!important}#ProductDetails .Attri' +
  1407. 'butesTableRowGroupTitle{border-bottom:1px dashed #444!important}.Discoun' +
  1408. 'tsTable,.ProductDescriptions,.fixedCategoryLink,.scHeading{border-bottom' +
  1409. ':1px dotted #444!important}#navbar,.FilterHeading,.FooterBanner,.PostFla' +
  1410. 'gs ul li,.tabs-min .ui-widget-header{border-bottom:1px solid #444!import' +
  1411. 'ant}.SingleLineTextBox:focus,.VerticalTextArea:focus{border-color:#444!i' +
  1412. 'mportant}.ForumThread .Signature,.Reviews .ReviewContent{border-left:5px' +
  1413. ' solid #444!important}.ForumsNavigation{border-right:0 solid #e0e0e0!imp' +
  1414. 'ortant}#ProductSpecifications,.FooterLeft,.navmenubutton_separator{borde' +
  1415. 'r-right:1px dotted #444!important}.PostFlags,td.PageNavigationalPanel{bo' +
  1416. 'rder-right:1px solid #444!important}.FilterEntry:hover,.LargeFilterEntry' +
  1417. ':hover,.ProductFilters .FilterCancellable,.ProductFilters .FilterSelecte' +
  1418. 'd,.subCategories li:hover,div.StaticPageLink_Selected{border-right:3px s' +
  1419. 'olid #444!important}#navbar .hdivider,.FooterBanner,.OrdersShipmentHeadi' +
  1420. 'ng,.PopoutPanel{border-top:1px solid #444!important}.BGShadow{box-shadow' +
  1421. ':0 1px 1px #666!important}.BGShadowLight{box-shadow:0 3px 3px #666!impor' +
  1422. 'tant}#CartPopout .ViewCart a:hover,#DMCA,#DMCA div.dm-1 a,#DMCA div.dm-2' +
  1423. ' a,#ProductDetailsPanel .Price,#navbar .focused a,#searchButtonCell,.Cel' +
  1424. 'lMode .GridItemPrice,.FieldFlag a,.FormButton,.FormButton.blue:hover,.Fo' +
  1425. 'rmButton.green:hover,.FormButton.orange:hover,.FormButton.purple:hover,.' +
  1426. 'FormButton.red:hover,.LineMode .GridItemPrice,.MemberRankIndex,.Pager .P' +
  1427. 'ageLink_Selected,.Pager .PageLink_Selected:hover,.Pager .PageLink_Select' +
  1428. 'ed:visited,.Steps .focused,.Steps .focused .StepText,.Steps .focused .St' +
  1429. 'epTitle,.tabs-min .ui-state-default a,.unitSelector{color:#444!important' +
  1430. '}.tabs-min .ui-state-active a{color:#666!important}#navbar .menulinks sp' +
  1431. 'an a:active,a,a:visited{color:#d5d5d5!important}#categoryButton,#searchB' +
  1432. 'arCategory,.CartTable .Name a,.CellMode .GridItemModels,.FormButton.whit' +
  1433. 'e,.FormButton.white:hover,.ForumThread .ThreadCommand,.Information,.Line' +
  1434. 'Mode .GridItemModels,.Pager .PageLink,.navmenubutton a,.subCategories a,' +
  1435. '.topLevelCategories a,.topLevelCategories li.focused a{color:#e1e1e1!imp' +
  1436. 'ortant}.Black{color:#e2e2e2!important}.PageTitle{color:#e5e5e5!important' +
  1437. '}#categoryButton a,.PostFlags .FlagValue,.SingleLineTextBox:focus,.Verti' +
  1438. 'calTextArea:focus{color:#f1f1f1!important}body{color:#f3f3f3!important}#' +
  1439. 'content_ProductName,.mbbc .mbbc-align-popup li,.mbbc .mbbc-code-popup li' +
  1440. ',.mbbc .mbbc-editor,.mbbc .mbbc-font-popup li,.mbbc .mbbc-list-popup li,' +
  1441. '.mbbc .mbbc-simple-popup li,.mbbc .mbbc-size-popup li,.subCategories a:h' +
  1442. 'over,.topLevelCategories a:hover,a:hover{color:#fff!important}.MemberRan' +
  1443. 'kUsername{font-weight:700!important}.ForumGroup{padding:3px 10px!importa' +
  1444. 'nt}.mbbc .mbbc-buttons ul{padding:5px 0 3px!important}.mbbc .mbbc-emotic' +
  1445. 'on-popup .mbbc-group li{padding:8px!important}');
  1446.  
  1447. settings.register('use_theme', 'Use custom theme', false, null);
  1448. settings.register('theme_name', null, 'nightmode', 'use_theme', {
  1449. nightmode: { name: 'Night mode' },
  1450. darkft: { name: 'Pafapaf\'s DarkFT' },
  1451. });
  1452. styles.update();
  1453. // Add a "Top of page" button at the bottom of the page, aligned with the pager
  1454. if ((ftl.thread && !ftl.perma) || (ftl.threadlist && !ftl.settings)) {
  1455. var add_top_btn;
  1456. if (!ftl.mobile) {
  1457. add_top_btn = function() {
  1458. var elm = document.getElementsByClassName('ForumThread TightTable')[0];
  1459. if (elm)
  1460. elm = elm.nextElementSibling;
  1461. if (!elm)
  1462. return;
  1463. elm.className = 'Pager';
  1464. var tbtn = document.createElement('span');
  1465. tbtn.className = 'ControlArrows';
  1466. tbtn.setAttribute('style', 'float: left; min-width: 150px; ' +
  1467. 'max-width: 250px; text-align: center');
  1468. tbtn.innerHTML = '<a>Top of page</a>';
  1469. tbtn.firstElementChild.href = 'javascript:void(0)';
  1470. handlers.top_click(tbtn);
  1471. var pager = elm.firstElementChild;
  1472. elm.appendChild(tbtn);
  1473. if (pager) {
  1474. var pspan = document.createElement('span');
  1475. pspan.appendChild(pager);
  1476. elm.appendChild(pspan);
  1477. } else {
  1478. elm.parentElement
  1479. .insertBefore(document.createElement('br'), elm.nextElementSibling);
  1480. }
  1481. };
  1482. } else {
  1483. add_top_btn = function() {
  1484. var elm = document.getElementsByClassName('ListRow')[0];
  1485. if (elm)
  1486. elm = elm.parentElement.nextElementSibling;
  1487. if (!elm)
  1488. return;
  1489. var tbtn = document.createElement('button');
  1490. tbtn.className = 'btn btn-default';
  1491. tbtn.innerHTML = 'Top';
  1492. handlers.top_click(tbtn);
  1493. if (elm.firstElementChild) {
  1494. tbtn.style.marginTop = '-5px';
  1495. elm = elm.firstElementChild;
  1496. var td = document.createElement('td');
  1497. td.style.textAlign = 'right';
  1498. td.appendChild(tbtn);
  1499. elm.style.width = '100%';
  1500. elm.firstElementChild.firstElementChild.appendChild(td);
  1501. } else {
  1502. var d = elm.appendChild(document.createElement('div'));
  1503. d.setAttribute('style', 'width: 100%; text-align: right; ' +
  1504. 'margin-bottom: 8px');
  1505. d.appendChild(tbtn);
  1506. }
  1507. };
  1508. }
  1509. block.watch('ready', add_top_btn);
  1510. }
  1511.  
  1512. if (ftl.editor) {
  1513. var bbeeditor; // element to scroll to
  1514. var bbetext; // textarea
  1515. var bbeexpand; // expand button (mobile)
  1516. var bbepanel; // parent of editor
  1517. if (!ftl.mobile) {
  1518. var fix_editor = function() {
  1519. if (bbetext === undefined)
  1520. bbetext = document.getElementsByClassName('mbbc-editor')[0];
  1521. if (bbetext !== undefined) {
  1522. // Save & restore the editor text
  1523. if (history.state && history.state.saved_post)
  1524. bbetext.value = history.state.saved_post;
  1525. window.addEventListener('pagehide', function() {
  1526. var state = history.state;
  1527. if (state === null)
  1528. state = {};
  1529. state.saved_post = bbetext.value;
  1530. history.replaceState(state, '');
  1531. });
  1532.  
  1533. // Remove the broken size=[67] buttons
  1534. var size_popup = document
  1535. .getElementsByClassName('mbbc-size-popup')[0];
  1536. if (size_popup !== undefined) {
  1537. size_popup.lastElementChild.remove();
  1538. size_popup.lastElementChild.remove();
  1539. }
  1540. // Fix the editor width
  1541. bbetext.style.boxSizing = 'border-box';
  1542. bbetext.style.width = '100%';
  1543. // Add a YouTube button
  1544. var url_btn = document.getElementsByClassName('mbbc-url')[0];
  1545. if (url_btn !== undefined) {
  1546. var yt_btn = document.createElement('li');
  1547. yt_btn.innerHTML = '&nbsp;';
  1548. yt_btn.className = 'mbbc-youtube';
  1549. yt_btn.addEventListener('click', function() {
  1550. var sp = bbetext.selectionStart;
  1551. var ep = bbetext.selectionEnd;
  1552. var sel;
  1553. if (ep < sp) {
  1554. sp = ep;
  1555. ep = bbetext.selectionStart;
  1556. }
  1557. if (ep !== sp) {
  1558. sel = bbetext.value.slice(sp, ep);
  1559. } else {
  1560. sel = prompt('Video URL:', '');
  1561. if (sel === null)
  1562. return;
  1563. var vid = sel
  1564. .match(new RegExp('[/?=#]([-\\w]{11})([^-\\w]|$)'));
  1565. if (vid !== null)
  1566. sel = 'https://www.youtube.com/watch?v=' + vid[1];
  1567. }
  1568. if (sel === undefined)
  1569. return;
  1570. bbetext.value = bbetext.value.slice(0, sp) +
  1571. '[youtube]' + sel + '[/youtube]' +
  1572. bbetext.value.slice(ep);
  1573. bbetext.selectionStart = bbetext.selectionEnd = sp + 9 +
  1574. (sel === '' ? 0 : 10 + sel.length);
  1575. bbetext.focus();
  1576. });
  1577. url_btn.parentElement.insertBefore(yt_btn, url_btn);
  1578. }
  1579. }
  1580. };
  1581.  
  1582. block.watch('late', function() {
  1583. // Persist the terms of use checkbox
  1584. var toubox = document.getElementById('AgreeTerms');
  1585. if (toubox !== null)
  1586. handlers.checkbox(toubox, 'agree_tou');
  1587.  
  1588. bbeeditor = document.getElementById('bbEditor');
  1589. if (bbeeditor !== null) {
  1590. bbetext = bbeeditor.getElementsByClassName('mbbc-editor')[0];
  1591. if (bbetext !== undefined)
  1592. fix_editor();
  1593. else
  1594. new MutationObserver(function(es, mo) {
  1595. mo.disconnect();
  1596. fix_editor();
  1597. }).observe(bbeeditor, { childList: true });
  1598. }
  1599. });
  1600. } else {
  1601. block.watch('ready', function() {
  1602. bbetext = document.getElementById('RawContent');
  1603. if (bbetext !== null) {
  1604. bbepanel = bbetext.parentElement.parentElement.parentElement;
  1605. bbeeditor = bbepanel.parentElement;
  1606. bbeexpand = bbeeditor.previousElementSibling.firstElementChild
  1607. .firstElementChild;
  1608. }
  1609. });
  1610. }
  1611. }
  1612.  
  1613. if (ftl.thread && !ftl.perma) {
  1614. // Re-implement quotes in a way that sort of works
  1615. var fix_quote_btns = (function() {
  1616.  
  1617. function wrap_tag(tag, val, str) {
  1618. return '[' + tag + (val ? '=' + val : '') + ']' + str + '[/' + tag + ']';
  1619. }
  1620.  
  1621. function inner_quote(elm, range, depth) {
  1622. var tmp = '';
  1623. for (var e = elm.firstChild; e !== null; e = e.nextSibling) {
  1624. var sp, ep;
  1625. if (range !== undefined) {
  1626. if (range.startContainer.compareDocumentPosition(e) === 2)
  1627. continue;
  1628. if (range.endContainer.compareDocumentPosition(e) === 4)
  1629. break;
  1630. if (range.startContainer === e)
  1631. sp = range.startOffset;
  1632. if (range.endContainer === e)
  1633. ep = range.endOffset;
  1634. }
  1635. switch (e.tagName) {
  1636. case 'BR':
  1637. tmp += '\n';
  1638. break;
  1639.  
  1640. case 'STRONG':
  1641. tmp += wrap_tag('b', '', inner_quote(e, range, depth));
  1642. break;
  1643.  
  1644. case 'EM':
  1645. tmp += wrap_tag('i', '', inner_quote(e, range, depth));
  1646. break;
  1647.  
  1648. case 'HR':
  1649. tmp += '[hr]';
  1650. break;
  1651.  
  1652. case 'P':
  1653. if (e.innerHTML !== '')
  1654. tmp += '\n' + inner_quote(e, range, depth);
  1655. break;
  1656.  
  1657. case 'SPAN':
  1658. var y = e.style;
  1659. var t = null;
  1660. var v = '';
  1661. if (y.textDecoration === 'underline') {
  1662. t = 'u';
  1663. } else if (y.textDecoration === 'line-through') {
  1664. t = 'y';
  1665. } else if (y.fontSize !== '') {
  1666. t = 'size';
  1667. v = parseInt(y.fontSize);
  1668. } else if (y.color !== '') {
  1669. t = 'color';
  1670. v = y.color;
  1671. } else if (y.textAlign !== '') {
  1672. t = 'align';
  1673. v = y.textAlign;
  1674. } else {
  1675. console.log('Unknown span!');
  1676. }
  1677.  
  1678. tmp += wrap_tag(t, v, inner_quote(e, range, depth));
  1679. break;
  1680.  
  1681. case 'DIV':
  1682. if (e.className === 'ForumQuote') {
  1683. if (tmp[tmp.length - 1] !== '\n')
  1684. tmp += '\n';
  1685. if (depth === 0 && settings.get('quote_quotes'))
  1686. tmp += wrap_tag('quote', '',
  1687. inner_quote(e.firstElementChild, range, depth + 1).trim());
  1688. } else {
  1689. console.log('Unknown div!');
  1690. }
  1691.  
  1692. break;
  1693.  
  1694. case 'IFRAME':
  1695. var m = e.src.match(/youtube[^\/]*.com.*\/embed\/([^?]+)/);
  1696. if (m !== null && m[1] !== undefined) {
  1697. /* A YouTube tag inside a quote breaks the parsing of the quote.
  1698. tmp += wrap_tag('youtube', '',
  1699. 'https://www.youtube.com/watch?v=' + m[1])
  1700. */
  1701. tmp += wrap_tag('url',
  1702. 'https://www.youtube.com/watch?v=' + m[1], 'YouTube video');
  1703. } else {
  1704. console.warn('Unknown iframe!');
  1705. }
  1706. break;
  1707.  
  1708. case 'IMG':
  1709. if (settings.get('quote_imgs')) {
  1710. tmp += wrap_tag('img', '', e.src);
  1711. } else if (e.parentElement.tagName !== 'A') {
  1712. tmp += wrap_tag('url', e.src, 'Image');
  1713. } else {
  1714. tmp += 'Image';
  1715. }
  1716. break;
  1717.  
  1718. case 'A':
  1719. if (e.className === 'SKUAutoLink') {
  1720. tmp += e.textContent;
  1721. } else {
  1722. var do_quote = true;
  1723. if ((depth > 0 || !settings.get('quote_quotes') &&
  1724. e.innerHTML.match(/\bwrote$/) !== null)) {
  1725. // Is this a permalink for a quote we'll remove?
  1726. var s = e;
  1727. var l;
  1728. do {
  1729. if (s.nextSibling === null) {
  1730. l = (l === undefined) ? s : l;
  1731. s = s.parentElement.nextSibling;
  1732. } else {
  1733. s = s.nextSibling;
  1734. }
  1735.  
  1736. /* Between the <a> and the <div>, we only want to see a
  1737. * colon and an unpredictable number of <br> and empty <p>
  1738. * elements. The <div> may be outside the current element.
  1739. */
  1740. if (s.tagName === 'DIV' && s.className === 'ForumQuote') {
  1741. do_quote = false;
  1742. e = (l === undefined) ? s : l;
  1743. break;
  1744. } else if (s.tagName === undefined) {
  1745. if (s.textContent.match(/[^:\r\n]/) !== null)
  1746. break;
  1747. } else if (s.innerHTML !== '' ||
  1748. (s.tagName !== 'P' && s.tagName !== 'BR')) {
  1749. break;
  1750. }
  1751. } while (true);
  1752. }
  1753. if (do_quote)
  1754. tmp += wrap_tag('url', e.href, inner_quote(e, range, depth));
  1755. }
  1756. break;
  1757.  
  1758. case undefined:
  1759. // Filter out newlines and empty tags
  1760. tmp += e.wholeText.slice(sp ? sp : 0, ep)
  1761. .replace(/[\r\n]+/mg, '')
  1762. .replace(/\[([^\]=]*)(=[^\]]*)?\]\[\/\1\]/mg, '');
  1763. break;
  1764.  
  1765. default:
  1766. console.warn('Unhandled tag ' + e.tagName);
  1767. }
  1768. }
  1769. return tmp;
  1770. }
  1771.  
  1772. function quote(user, elm) {
  1773. if (bbetext === null) {
  1774. console.error('bbEditor text box not found!');
  1775. return;
  1776. }
  1777. var post_id;
  1778. var pid = elm.id.match(/^POST([0-9]+)$/);
  1779. if (pid !== null)
  1780. post_id = pid[1];
  1781.  
  1782. var sel = getSelection();
  1783. if (sel.rangeCount > 0) {
  1784. sel = sel.getRangeAt(0);
  1785. if (sel.collapsed || !sel.intersectsNode(elm))
  1786. sel = undefined;
  1787. } else {
  1788. sel = undefined;
  1789. }
  1790.  
  1791. if (bbetext.value.length > 0 &&
  1792. bbetext.value[bbetext.value.length - 1] !== '\n')
  1793. bbetext.value += '\n';
  1794. if (thread_id !== undefined && post_id !== undefined)
  1795. bbetext.value += wrap_tag('url', '/forums/-/t/' + thread_id + '?' +
  1796. post_id, user + ' wrote') + ':\n';
  1797. else
  1798. bbetext.value += user + ' wrote:\n';
  1799. bbetext.value += wrap_tag('quote', '', inner_quote(elm, sel, 0)
  1800. .trim().replace(/[\r\n]{3,}/mg, '\n\n')) + '\n';
  1801.  
  1802. if (bbeeditor)
  1803. bbeeditor.scrollIntoView();
  1804.  
  1805. if (bbeexpand && bbepanel && bbepanel.className &&
  1806. !/\bin\b/.test(bbepanel.className))
  1807. bbeexpand.click();
  1808. }
  1809.  
  1810. var quote_click = handlers.generic({ onclick: function() {
  1811. quote(this.data.getAttribute('data-username'), this.data);
  1812. }});
  1813.  
  1814. return function(elms, func) {
  1815. for (var i = 0; i < elms.length; i++) {
  1816. var elm = elms[i].content_elm;
  1817. quote_click(func(elm), elm);
  1818. }
  1819. };
  1820. })();
  1821.  
  1822. if (!ftl.mobile) {
  1823. var replace_quotebtn = function(elm) {
  1824. // Ugh.
  1825. var oldbtn = elm.parentElement.parentElement.parentElement
  1826. .previousElementSibling.firstElementChild.lastElementChild
  1827. .firstElementChild;
  1828. var newbtn = document.createElement('a');
  1829. newbtn.innerHTML = 'quote/reply';
  1830. newbtn.href = 'javascript:void(0)';
  1831. oldbtn.parentElement.insertBefore(newbtn, oldbtn);
  1832. oldbtn.remove();
  1833. return newbtn;
  1834. };
  1835.  
  1836. block.watch('late', function() {
  1837. fix_quote_btns(posts, replace_quotebtn);
  1838. });
  1839. } else { // mobile
  1840. var replace_quotebtn_m = function(elm) {
  1841. // Ugh.
  1842. var oldbtn = elm.parentElement.firstElementChild.lastElementChild
  1843. .lastElementChild.firstElementChild;
  1844. var newbtn = document.createElement('a');
  1845. newbtn.innerHTML = oldbtn.innerHTML;
  1846. newbtn.href = 'javascript:void(0)';
  1847. newbtn.tabindex = '-1';
  1848. newbtn.role = 'menuitem';
  1849. oldbtn.parentElement.insertBefore(newbtn, oldbtn);
  1850. oldbtn.remove();
  1851. return newbtn;
  1852. };
  1853.  
  1854. block.watch('late', function() {
  1855. fix_quote_btns(posts, replace_quotebtn_m);
  1856.  
  1857. // Add a link to forum settings to allow ignorelist management
  1858. // Ugh. There's no semi-robust way to select this.
  1859. var li = document.querySelector('ul.nav:nth-child(1)>li:nth-child(3)' +
  1860. '>ul:nth-child(2)>li:nth-child(8)');
  1861. if (li !== null) {
  1862. var new_li = document.createElement('li');
  1863. new_li.innerHTML = '<a title="Forum Settings" ' +
  1864. 'href="https://www.fasttech.com/forums/settings">' +
  1865. 'Forum Settings</a>';
  1866. li.parentElement.insertBefore(new_li, li);
  1867. }
  1868. });
  1869. }
  1870. }
  1871.  
  1872. if (ftl.thread && ftl.mobile) {
  1873. // Fix up YT iframe size: CSS doesn't work well here.
  1874. var fr_resize = (function() {
  1875. var frs;
  1876. var raf;
  1877. function do_resize() {
  1878. for (var i = 0; i < frs.length; i++) {
  1879. var fr = frs[i];
  1880. fr.elm.style.height = Math.ceil(fr.elm.clientWidth * fr.ar) + 'px';
  1881. }
  1882.  
  1883. raf = undefined;
  1884. }
  1885. return function(fr) {
  1886. if (frs === undefined) {
  1887. frs = [];
  1888. window.addEventListener('resize', function() {
  1889. if (raf === undefined)
  1890. raf = requestAnimationFrame(do_resize);
  1891. });
  1892. }
  1893. fr.style.maxWidth = fr.width + 'px';
  1894. fr.style.width = '100%';
  1895. frs.push({elm: fr, ar: fr.height / fr.width});
  1896. };
  1897. })();
  1898.  
  1899. block.watch('ready', function() {
  1900. var frs = document.getElementsByTagName('iframe');
  1901. for (var i = 0; i < frs.length; i++)
  1902. fr_resize(frs[i]);
  1903. });
  1904. }
  1905. if (ftl.thread && !ftl.perma) {
  1906. var scroll_to_hash = function() {
  1907. function scroll(elm) {
  1908. if (elm) {
  1909. var sd = elm.style.display;
  1910. elm.style.display = '';
  1911. elm.scrollIntoView();
  1912. elm.style.display = sd;
  1913. }
  1914. }
  1915. if (location.hash === '#unread' || location.hash === '#last')
  1916. last_viewed.get(thread_id, function(hash, lvp, lpn, lp) {
  1917. block.watch('filter', function() {
  1918. var i;
  1919. var pid = 'POST' + lp;
  1920. for (i = posts.length - 1; i >= 0; i--) {
  1921. if (posts[i].content_elm.id === pid)
  1922. break;
  1923. }
  1924. if (i < posts.length - 1)
  1925. scroll(posts[i + 1].post_elms[0]);
  1926. else if (hash === '#last')
  1927. scroll(posts[posts.length - 1].post_elms[0]);
  1928. });
  1929. }, location.hash);
  1930. else if (location.hash !== '')
  1931. block.watch('filter', function() {
  1932. scroll(document.getElementById(location.hash));
  1933. });
  1934. // Drop the hash so we won't run again
  1935. history.replaceState(history.state, '', location.pathname);
  1936. };
  1937. }
  1938.  
  1939. if (ftl.threadlist) {
  1940. if (!ftl.mobile) {
  1941. var add_last_viewed =
  1942. function(elm, last_viewed_page, last_post_nr, last_post_id) {
  1943. var row = elm.parentElement;
  1944. var pelm = row.parentElement.getElementsByClassName('Pager')[0];
  1945. var p = parse_pager(pelm);
  1946. var skip_margin = false;
  1947.  
  1948. if (pelm === undefined) {
  1949. skip_margin = true;
  1950. if (!ftl.settings) {
  1951. pelm = document.createElement('div');
  1952. pelm.className = 'Pager';
  1953. row.appendChild(pelm);
  1954. } else {
  1955. pelm = row.parentElement.appendChild(document.createElement('div'));
  1956. pelm.className = 'Pager';
  1957. }
  1958. }
  1959.  
  1960. if (settings.get('track_viewed')) {
  1961. var lv = document.createElement('a');
  1962. lv.href = gen_page_link(elm.href, last_viewed_page);
  1963. lv.innerHTML = 'Last viewed';
  1964. lv.className = 'FormButton blue';
  1965. if (!skip_margin)
  1966. lv.style.marginLeft = '5px';
  1967. pelm.appendChild(lv);
  1968. skip_margin = false;
  1969. }
  1970.  
  1971. var tr = row.parentElement.parentElement;
  1972. var ts = tr.getElementsByClassName('ThreadStats')[0];
  1973. var pc;
  1974. if (ts !== undefined)
  1975. pc = (pc = ts.innerHTML.match(/Replies: ([0-9]+)/)) ?
  1976. parseInt(pc[1]) : undefined;
  1977. var unread = pc > 0 && pc >= last_post_nr;
  1978. if ((unread || settings.get('always_unread')) &&
  1979. settings.get('track_posts')) {
  1980. var np = document.createElement('a');
  1981. if (unread) {
  1982. np.innerHTML = 'Unread posts';
  1983. np.className = 'FormButton red';
  1984. np.href = gen_page_link(elm.href,
  1985. postnr_to_pagenr(last_post_nr, p.last), 'unread');
  1986. } else {
  1987. np.innerHTML = 'No unread posts';
  1988. np.className = 'FormButton white';
  1989. np.href = gen_page_link(elm.href,
  1990. postnr_to_pagenr(last_post_nr, p.last), 'last');
  1991. }
  1992. if (!skip_margin)
  1993. np.style.marginLeft = '5px';
  1994. pelm.appendChild(np);
  1995. skip_margin = false;
  1996. }
  1997. };
  1998.  
  1999. var mangle_list = function() {
  2000. var header = document.getElementsByClassName('ForumHeader')[0];
  2001. var filter = !ftl.settings && !ftl.author && settings.get('hide_threads');
  2002. var sku_re = new RegExp('fasttechcdn.com/[0-9]+/([0-9]+)/.*');
  2003. var author_re = new RegExp('started\\W+by\\W+(\\w+)');
  2004. if (header !== undefined) {
  2005. var ch = header.parentElement.children;
  2006. for (var i = 1; i < ch.length; i += 2) {
  2007. var tr = ch[i];
  2008. var tl = tr.getElementsByClassName('ThreadLink')[0];
  2009. var tid;
  2010. if (tl !== undefined &&
  2011. (settings.get('track_posts') || settings.get('track_viewed'))) {
  2012. tid = parseInt(tl.href.match(thread_id_re)[1]);
  2013. if (!isNaN(tid))
  2014. last_viewed.get(tid, add_last_viewed, tl);
  2015. }
  2016. var img = tr.firstElementChild.firstElementChild;
  2017. if (img && img.tagName === 'IMG') {
  2018. var sku = img.src.match(sku_re);
  2019. if (sku !== null) {
  2020. var p = img.parentElement;
  2021. var a = document.createElement('a');
  2022. a.href = '/products/' + sku[1];
  2023. a.appendChild(img);
  2024. p.appendChild(a);
  2025. }
  2026. }
  2027. if (filter) {
  2028. var u = tl.parentElement.textContent
  2029. .match(author_re);
  2030. if (u && u[1] && ignore_list.get(u[1])) {
  2031. tr.style.display = 'none';
  2032. if (tr.previousElementSibling !== header)
  2033. tr.previousElementSibling.style.display = 'none';
  2034. }
  2035. }
  2036. }
  2037. }
  2038. };
  2039.  
  2040. block.watch('ready', mangle_list);
  2041. } else { // mobile
  2042. var add_last_viewed =
  2043. function(elm, last_viewed_page, last_post_nr, last_post_id) {
  2044. var rh = elm.parentElement;
  2045. var tr = rh.parentElement;
  2046. var p = parse_pager(tr.getElementsByClassName('pagination')[0]);
  2047. var d = document.createElement('div');
  2048. d.style.marginLeft = '-5px';
  2049.  
  2050. if (settings.get('track_viewed')) {
  2051. var lv = document.createElement('span');
  2052. lv.innerHTML = '<a href="' +
  2053. gen_page_link(elm.href, last_viewed_page) + '">Last viewed</a>';
  2054. lv.className = 'btn btn-default';
  2055. lv.style.margin = '5px';
  2056. d.appendChild(lv);
  2057. }
  2058.  
  2059. var pc = (pc = tr.textContent.match(/R(eplies)?: ([0-9]+)/)) ?
  2060. parseInt(pc[2]) : undefined;
  2061. var unread = pc > 0 && pc >= last_post_nr;
  2062. if ((unread || settings.get('always_unread')) &&
  2063. settings.get('track_posts')) {
  2064. var np = document.createElement('span');
  2065. np.className = 'btn btn-default';
  2066. np.style.margin = '5px';
  2067. var npa = document.createElement('a');
  2068. if (unread) {
  2069. npa.innerHTML = 'Unread posts';
  2070. npa.href = gen_page_link(elm.href,
  2071. postnr_to_pagenr(last_post_nr, p.last), 'unread');
  2072. npa.style.color = '#C64148';
  2073. } else {
  2074. npa.innerHTML = 'No unread posts';
  2075. npa.style.color = '#888';
  2076. npa.href = gen_page_link(elm.href,
  2077. postnr_to_pagenr(last_post_nr, p.last), 'last');
  2078. }
  2079. np.appendChild(npa);
  2080. d.appendChild(np);
  2081. }
  2082.  
  2083. rh.appendChild(d);
  2084. };
  2085.  
  2086. var mangle_list = function() {
  2087. var filter = settings.get('hide_threads');
  2088. var elms = document.getElementsByClassName('ListRow');
  2089. var author_re = new RegExp('started\\W+by\\W+(\\w+)');
  2090. for (var i = 0; i < elms.length; i++) {
  2091. var elm = elms[i];
  2092. var rh = elm.getElementsByClassName('RowHeading')[0];
  2093. if (rh === undefined)
  2094. continue;
  2095. var l = rh.firstElementChild;
  2096. if (l !== undefined &&
  2097. (settings.get('track_posts') || settings.get('track_viewed'))) {
  2098. var tid = parseInt(l.href.match(thread_id_re)[1]);
  2099. if (!isNaN(tid))
  2100. last_viewed.get(tid, add_last_viewed, l);
  2101. }
  2102.  
  2103. if (filter) {
  2104. var u = rh.parentElement.textContent.match(author_re);
  2105. if (u && u[1] && ignore_list.get(u[1]))
  2106. elm.style.display = 'none';
  2107. }
  2108. }
  2109. };
  2110. block.watch('ready', function() {
  2111. if (ftl.product) {
  2112. // Wait for thread list to load before mangling
  2113. var div = document.getElementById('forum');
  2114. if (div !== null)
  2115. new MutationObserver(function(es, mo) {
  2116. mo.disconnect();
  2117. mangle_list();
  2118. }).observe(div, { childList: true });
  2119. } else {
  2120. mangle_list();
  2121. }
  2122. });
  2123. }
  2124. }
  2125. if (!ftl.mobile && ftl.thread) {
  2126. var have_itmenu = false;
  2127. var have_itmanage = false;
  2128. var update_itmanage = function(n, itm) {
  2129. if (!have_itmenu)
  2130. return;
  2131. var div = document.getElementById('IgnorelistPopout');
  2132. if (div === null)
  2133. return;
  2134.  
  2135. if (!have_itmanage && itm) {
  2136. var frag = document.createDocumentFragment();
  2137. var hr = document.createElement('hr');
  2138. hr.className = 'itmanage';
  2139. frag.appendChild(hr);
  2140.  
  2141. var l = document.createElement('div');
  2142. l.innerHTML = '<b>Ignored Users</b>';
  2143. l.className = 'itmanage';
  2144. l.style.marginTop = '10px';
  2145. manage_menu.register(l);
  2146. l.getElementsByClassName('il_addbtn')[0].className = 'FormButton blue';
  2147. frag.appendChild(l);
  2148. div.appendChild(frag);
  2149. have_itmanage = true;
  2150. } else if (have_itmanage) {
  2151. var elms = div.getElementsByClassName('itmanage');
  2152. for (var i = 0; i < elms.length; i++)
  2153. elms[i].style.display = itm ? '' : 'none';
  2154. }
  2155. };
  2156. var update_itmenu = function(n, itm) {
  2157. if (!have_itmenu && itm) {
  2158. var b = document.getElementsByClassName('ThreadCommandBar')[0];
  2159. var sp = document.createElement('span');
  2160. sp.id = 'Ignorelist';
  2161. sp.className = 'ThreadCommand';
  2162. sp.innerHTML = 'Settings';
  2163. b.appendChild(sp);
  2164.  
  2165. block.watch('late', function() {
  2166. var div = document.createElement('div');
  2167. div.id = 'IgnorelistPopout';
  2168. div.className = 'PopoutPanel';
  2169. div.align = 'left';
  2170. div.setAttribute('style',
  2171. 'position: absolute; background: white; padding: 20px');
  2172. settings_menu.register(div, thread_menu);
  2173. b.appendChild(div);
  2174.  
  2175. PM2({par: 'Ignorelist', dupe: false});
  2176.  
  2177. have_itmenu = true;
  2178. block.watch('late', function() {
  2179. update_itmanage(undefined, settings.get('inthread_manage'));
  2180. });
  2181. });
  2182. } else if (have_itmenu) {
  2183. var elm = document.getElementById('Ignorelist');
  2184. if (elm !== null)
  2185. elm.style.display = itm ? '' : 'none';
  2186. }
  2187. };
  2188.  
  2189. post_filter.watch(gen_watch_filter(function(post) {
  2190. var tbl = post.post_elms[0].parentElement;
  2191. var ph = post.placeholder = document.createElement('tr');
  2192. ph.innerHTML = '<td colspan="2" class="ForumHeader" ' +
  2193. 'style="padding: 10px; text-align: center"><a>Post by ' +
  2194. strip_tags(post.user) + ' hidden. Click to show.</a></td>';
  2195. ph.firstElementChild.firstElementChild.href = 'javascript:void(0)';
  2196. handlers.hide_toggle(ph.firstElementChild.firstElementChild, post);
  2197. tbl.insertBefore(ph, post.post_elms[0]);
  2198. return ph;
  2199. }));
  2200. settings.watch('placeholders', post_filter.update);
  2201.  
  2202. block.watch('ready', function() {
  2203. // Wire posts & ignorebuttons up
  2204. ignore_buttons.set('+ ignore', '+ unignore');
  2205. for (var i = 0; i < posts.length; i++) {
  2206. var post = posts[i];
  2207. var tl = post.content_elm.parentElement.parentElement.parentElement;
  2208. post.post_elms = [tl.previousElementSibling, tl];
  2209. var head = tl.previousElementSibling.firstElementChild.lastElementChild;
  2210. var new_a = document.createElement('a');
  2211. new_a.href = 'javascript:void(0)';
  2212. head.appendChild(new_a);
  2213. post.ignbtn = new_a;
  2214. post.badge = tl.getElementsByClassName('Badges')[0];
  2215. }
  2216. post_filter.register(posts);
  2217. ignore_buttons.register(posts);
  2218. user_tags.register(posts);
  2219.  
  2220. settings.watch('inthread_manage', update_itmanage);
  2221. settings.watch('inthread_menu', update_itmenu);
  2222. });
  2223.  
  2224. block.watch('late', function() {
  2225. var p = parse_pager(document.getElementsByClassName('Pager')[1]);
  2226. var lpn = (p.cur - 1) * 10 + posts.length;
  2227. var lp = parseInt(posts[posts.length - 1].content_elm.id.slice(4));
  2228. if (!ftl.perma)
  2229. last_viewed.set(thread_id, p.cur, lpn, lp);
  2230. });
  2231.  
  2232. if (!ftl.perma && thread_id !== undefined) {
  2233. if (location.hash !== '')
  2234. scroll_to_hash();
  2235. }
  2236. }
  2237. if (ftl.mobile && ftl.thread) {
  2238. post_filter.watch(gen_watch_filter(function(post) {
  2239. var tbl = post.post_elms[0].parentElement;
  2240. var ph = post.placeholder = document.createElement('div');
  2241. ph.className = 'col-xs-12 ListRow';
  2242. ph.style.paddingBottom = '5px';
  2243. ph.style.textAlign = 'center';
  2244. ph.innerHTML = '<a>Post by ' + strip_tags(post.user) +
  2245. ' hidden. Click to show.</a>';
  2246. ph.firstElementChild.href = 'javascript:void(0)';
  2247. handlers.hide_toggle(ph.firstElementChild, post);
  2248. tbl.insertBefore(ph, post.post_elms[0]);
  2249. return ph;
  2250. }));
  2251. settings.watch('placeholders', post_filter.update);
  2252.  
  2253. ignore_buttons.set(' Ignore', ' Unignore');
  2254.  
  2255. var have_itmenu = false;
  2256. var have_itmanage = false;
  2257. var update_itmanage = function(n, itm) {
  2258. if (!have_itmenu)
  2259. return;
  2260. var menu = document.getElementById('hideSettings');
  2261. if (menu === null)
  2262. return;
  2263. if (!have_itmanage && itm) {
  2264. var frag = document.createDocumentFragment();
  2265. var inner = menu.getElementsByClassName('panel-body')[0];
  2266. var hr = document.createElement('hr');
  2267. hr.className = 'itmanage';
  2268. frag.appendChild(hr);
  2269. var l = document.createElement('div');
  2270. l.className = 'itmanage';
  2271. l.innerHTML = '<b>Ignored Users</b>';
  2272. l.style.marginTop = '10px';
  2273. manage_menu.register(l);
  2274. l.getElementsByClassName('il_addbtn')[0].remove();
  2275. frag.appendChild(l);
  2276. inner.appendChild(frag);
  2277. have_itmanage = true;
  2278. } else if (have_itmanage) {
  2279. var elms = menu.getElementsByClassName('itmanage');
  2280. for (var i = 0; i < elms.length; i++)
  2281. elms[i].style.display = itm ? '' : 'none';
  2282. }
  2283. };
  2284. var update_itmenu = function(n, itm) {
  2285. if (!have_itmenu && itm) {
  2286. var div = document.createElement('div');
  2287. div.id = 'itmenu';
  2288. div.className = 'panel panel-default';
  2289. div.innerHTML = '<div class="panel-heading"><h5 class="panel-title">' +
  2290. '<a href="#hideSettings" data-toggle="collapse">Settings ' +
  2291. '<span class="caret"></span></a></h5>' +
  2292. '<div id="hideSettings" class="panel-collapse collapse">' +
  2293. '<div class="panel-body"></div></div>';
  2294. block.watch('late', function() {
  2295. var inner = div.getElementsByClassName('panel-body')[0];
  2296. settings_menu.register(inner, thread_menu);
  2297. document.getElementsByClassName('body-content')[0].appendChild(div);
  2298.  
  2299. have_itmenu = true;
  2300. if (settings.get('inthread_manage') !== undefined)
  2301. update_itmanage(undefined, settings.get('inthread_manage'));
  2302. });
  2303. } else if (have_itmenu) {
  2304. document.getElementById('itmenu').style.display = itm ? '' : 'none';
  2305. }
  2306. };
  2307.  
  2308. block.watch('ready', function() {
  2309. for (var i = 0; i < posts.length; i++) {
  2310. var post = posts[i];
  2311. var pe = post.content_elm.parentElement;
  2312. post.post_elms = [pe];
  2313. var menu = pe.firstElementChild.lastElementChild;
  2314.  
  2315. var li = document.createElement('li');
  2316. li.setAttribute('role', 'presentation');
  2317. li.innerHTML = '<a tabindex="-1" role="menuitem">' +
  2318. '<span class="glyphicon glyphicon-plus red"></span><span></span></a>';
  2319. li.firstElementChild.href = 'javascript:void(0)';
  2320. var rp = menu.lastElementChild;
  2321. if (!rp.firstElementChild.lastChild.data.startsWith(' Report'))
  2322. rp = rp.previousElementSibling;
  2323. menu.insertBefore(li, rp);
  2324. post.ignbtn = li.firstElementChild.lastElementChild;
  2325.  
  2326. var badge = menu.firstElementChild.firstElementChild;
  2327. var m = badge.innerHTML.match(/(.*)\((.*)\)/);
  2328. badge.innerHTML = m[1] + '(<span>' + m[2] + '</span>)';
  2329. post.badge = badge.lastElementChild;
  2330. }
  2331. post_filter.register(posts);
  2332. ignore_buttons.register(posts);
  2333. user_tags.register(posts);
  2334.  
  2335. settings.watch('inthread_manage', update_itmanage);
  2336. settings.watch('inthread_menu', update_itmenu);
  2337. });
  2338.  
  2339. block.watch('late', function() {
  2340. var p = parse_pager(document.getElementsByClassName('pagination')[0]);
  2341. var lpn = (p.cur - 1) * 10 + posts.length;
  2342. var lp = parseInt(posts[posts.length - 1].content_elm.id.slice(4));
  2343. if (!ftl.perma)
  2344. last_viewed.set(thread_id, p.cur, lpn, lp);
  2345. });
  2346.  
  2347. if (!ftl.perma && thread_id !== undefined) {
  2348. if (location.hash !== '')
  2349. scroll_to_hash();
  2350. }
  2351. }
  2352. if (ftl.new_arrivals) {
  2353. var product_filter = (function() {
  2354. var items;
  2355.  
  2356. function update() {
  2357. if (items === undefined)
  2358. return;
  2359.  
  2360. for (var i = 0; i < items.length; i++) {
  2361. items[i].content_elm.style.display =
  2362. ignore_categories.get(items[i].category) ? 'none' : '';
  2363. }
  2364. }
  2365.  
  2366. return {
  2367. register: function(list) {
  2368. items = list;
  2369. ignore_categories.watch('late', update);
  2370. },
  2371. };
  2372. })();
  2373.  
  2374. var catnr_re = new RegExp('categor(y|ies)/([0-9]+)/');
  2375. var catstr_re = new RegExp('\\bf=([a-zA-Z0-9]+)');
  2376. var catstr_cleanup_re = new RegExp('^.*=([0-9]+)$');
  2377. var ignore_category = function(elm, cat) {
  2378. elm.addEventListener('click', function() { ignore_categories.set(cat); });
  2379. ignore_categories.watch('late', function(c, s) {
  2380. if (c === cat)
  2381. elm.checked = !s;
  2382. });
  2383. elm.checked = !ignore_categories.get(cat);
  2384. };
  2385. var add_ign_cb = function(elm) {
  2386. try {
  2387. var cat = atob(elm.lastElementChild.href.match(catstr_re)[1])
  2388. .match(catstr_cleanup_re)[1];
  2389.  
  2390. var cb = document.createElement('input');
  2391. cb.type = 'checkbox';
  2392. cb.style.float = 'right';
  2393. cb.style.margin = '2px -2px 0px -11px';
  2394. ignore_category(cb, cat);
  2395. elm.insertBefore(cb, elm.firstElementChild);
  2396. } catch (e) {}
  2397. };
  2398.  
  2399. block.watch('ready', function() {
  2400. var container = ftl.mobile ?
  2401. document.getElementsByClassName('ProductsGrid')[0] :
  2402. document.getElementById('Products_Grid');
  2403. if (container === null || container === undefined)
  2404. return;
  2405.  
  2406. var list = [];
  2407. for (var elm = container.firstElementChild; elm !== null;
  2408. elm = elm.nextElementSibling) {
  2409. try {
  2410. var cat = elm.lastElementChild.lastElementChild.firstElementChild.href
  2411. .match(catnr_re)[2];
  2412. list.push({ content_elm: elm, category: cat });
  2413. } catch (e) {}
  2414. }
  2415. product_filter.register(list);
  2416.  
  2417. var cats, i;
  2418. if (ftl.mobile) {
  2419. var heads = document.getElementsByClassName('panel-heading');
  2420. for (i = 0; i < heads.length; i++) {
  2421. if (heads[i].textContent.trim() === 'Category') {
  2422. cats = heads[i];
  2423. break;
  2424. }
  2425. }
  2426. if (cats === undefined)
  2427. return;
  2428.  
  2429. cats = cats.parentElement.lastElementChild.firstElementChild;
  2430. while (cats !== null) {
  2431. add_ign_cb(cats.lastElementChild);
  2432. cats = cats.nextElementSibling;
  2433. }
  2434. } else {
  2435. var groups = document.getElementsByClassName('FilterGroup');
  2436.  
  2437. for (i = 0; i < groups.length; i++) {
  2438. if (groups[i].textContent.trim() === 'Category') {
  2439. cats = groups[i];
  2440. break;
  2441. }
  2442. }
  2443. if (cats === undefined)
  2444. return;
  2445.  
  2446. cats = cats.nextElementSibling;
  2447. while (cats !== null && cats.classList.contains('FilterEntry')) {
  2448. add_ign_cb(cats);
  2449. cats = cats.nextElementSibling;
  2450. }
  2451. }
  2452. });
  2453. }
  2454. // Mobile reviews don't show tags
  2455. if (ftl.reviews && !ftl.mobile) {
  2456. var reg_tags = function(badges) {
  2457. var posts = [];
  2458. for (var i in badges) {
  2459. var badge = badges[i];
  2460. if (badge.innerText === '' ||
  2461. !badge.previousElementSibling ||
  2462. badge.previousElementSibling.className !== 'Nickname')
  2463. continue;
  2464. posts.push({
  2465. user: badge.previousElementSibling.innerText.trim(),
  2466. badge: badge,
  2467. });
  2468. }
  2469. if (posts.length !== 0)
  2470. user_tags.register(posts);
  2471. };
  2472.  
  2473. block.watch('ready', function() {
  2474. var badges = document.getElementsByClassName('Badges');
  2475. if (badges.length !== 0) {
  2476. reg_tags(badges);
  2477. } else {
  2478. var elm;
  2479. if (!ftl.mobile)
  2480. elm = document.getElementById('Reviews-tab');
  2481. else
  2482. elm = document.getElementById('reviews');
  2483. if (elm !== null) {
  2484. new MutationObserver(function(es, mo) {
  2485. mo.disconnect();
  2486. reg_tags(document.getElementsByClassName('Badges'));
  2487. }).observe(elm, { childlist: true });
  2488. }
  2489. }
  2490. });
  2491. }
  2492. if (!ftl.mobile) {
  2493. if (ftl.prsearch) {
  2494. var search = location.href.match(/\/search(\/([0-9]+)?-?(.*))?\?([^&]+)/);
  2495. if (search !== null) {
  2496. if (!search[2])
  2497. search[2] = '0';
  2498. var sortcookie = document.cookie.match(/\bsort=([a-zA-Z0-9]+)/);
  2499. sortcookie = sortcookie ? sortcookie[1] : 'r';
  2500. location.replace(location.origin +
  2501. '/category/' + search[2] + '/search/-/p/0?f=-&keywords=' + search[4] +
  2502. '&sort=' + sortcookie);
  2503. }
  2504. }
  2505.  
  2506. if (ftl.prcategory && location.search) {
  2507. block.watch('ready', function() {
  2508. var sortelm = document.getElementById('content_SortOrder');
  2509. if (sortelm) {
  2510. sortelm.onchange = function() {
  2511. document.cookie = 'sort=' + this.value + ';path=/';
  2512. location.href = location.href.replace(/\bsort=[^&]*/, '') +
  2513. '&sort=' + this.value;
  2514. };
  2515. }
  2516.  
  2517. var searchcat = document.getElementById('searchBarCategory');
  2518. var urlcat = location.search.match(/\bf=([a-zA-Z0-9]+)/);
  2519. if (searchcat !== null && urlcat !== null) {
  2520. searchcat.value = atob(urlcat[1]).replace(/^.*=/, '');
  2521. if (!searchcat.value)
  2522. searchcat.value = '';
  2523. }
  2524. });
  2525. }
  2526. } else {
  2527. if (ftl.prsearch) {
  2528. var search = location.href.match(/\/search\/([0-9]+).*\?(.*)/);
  2529. if (search !== null) {
  2530. var sortcookie = document.cookie.match(/\bsort=([a-zA-Z0-9]+)/);
  2531. sortcookie = sortcookie ? sortcookie[1] : '0';
  2532. location.replace(location.origin +
  2533. '/c/' + search[1] + '/-/-/p/0?f=-&sort=' + sortcookie +
  2534. '&' + search[2]);
  2535. }
  2536. }
  2537.  
  2538. if (ftl.prcategory) {
  2539. var fix_onclick = function(elm, sortnr) {
  2540. elm.onclick = function() {
  2541. document.cookie = 'sort=' + sortnr + ';path=/';
  2542. location.href = location.href.replace(/\bsort=[^&]*/, '') +
  2543. '&sort=' + sortnr;
  2544. };
  2545. };
  2546.  
  2547. block.watch('ready', function () {
  2548. try {
  2549. var li = document.querySelector('.clearfix button .glyphicon-sort')
  2550. .parentElement.parentElement.lastElementChild.firstElementChild;
  2551. for ( ; li !== null; li = li.nextElementSibling) {
  2552. console.log(li);
  2553. var inner = li.firstElementChild;
  2554. var sortnr = String.match(inner.onclick, /\bsort=([0-9]+)/);
  2555. if (sortnr === null)
  2556. continue;
  2557. fix_onclick(inner, sortnr[1]);
  2558. }
  2559. } catch (e) {}
  2560. });
  2561. }
  2562. }
  2563. if (ftl.settings) {
  2564. block.watch('ready', function() {
  2565. var panel = document.getElementsByClassName('PageContentPanel')[0];
  2566. var div = document.createElement('div');
  2567. div.style.marginTop = '5px';
  2568. div.innerHTML = '<table style="width: auto"><tbody><tr><td class="MediumL' +
  2569. 'abel Bold EndOfInlineSection" style="padding: 5px">Ignored Users</td' +
  2570. '><td width=15></td><td class="MediumLabel Bold EndOfInlineSection" s' +
  2571. 'tyle="padding: 5px">FastTech Forum Enhancements Settings</td></tr><t' +
  2572. 'r><td style="padding: 0px"><div class="BGShadow" style="padding: 10p' +
  2573. 'x"></div></td><td width=15></td><td style="padding:10px"><div class=' +
  2574. '"BGShadow" style="padding: 0px"></div></td></tr></tbody></table>';
  2575.  
  2576. var divs = div.getElementsByTagName('div');
  2577. settings_menu.register(divs[1], full_menus);
  2578. manage_menu.register(divs[0]);
  2579. divs[0].getElementsByClassName('il_addbtn')[0].className =
  2580. 'FormButton blue';
  2581. panel.appendChild(div);
  2582. });
  2583. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址