Sleazy Fork is available in English.

E-Hentai Highlighter

Highlighter for E-Hentai (e-hentai.org/exhentai.org). Supports regular expressions.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name E-Hentai Highlighter
  3. // @namespace http://userscripts.org/users/106844
  4. // @description Highlighter for E-Hentai (e-hentai.org/exhentai.org). Supports regular expressions.
  5. // @include http://e-hentai.org/*
  6. // @include https://e-hentai.org/*
  7. // @include http://g.e-hentai.org/*
  8. // @include https://g.e-hentai.org/*
  9. // @include http://exhentai.org/*
  10. // @include https://exhentai.org/*
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @version 0.5.11.1
  14. // ==/UserScript==
  15.  
  16. // -------------------- DEFAULTS -------------------
  17.  
  18. var defaults = {
  19. defaultColor : '#ec7e7e' ,
  20. exColor : '#ed6464' ,
  21. highlighterEnabled : false ,
  22. filterEnabled : false ,
  23. opacityEnabled : false ,
  24. opacity : 0.1 ,
  25. showTags : true ,
  26. highlightTags : true ,
  27. reorderGalleries : true ,
  28. hideRatedGalleries : false ,
  29. showHideOption : true ,
  30. smartForegrounds : false ,
  31. chronologicalOrder : false ,
  32. };
  33.  
  34. // -------------------- /DEFAULTS -------------------
  35.  
  36. var EHH = {
  37. init: function() {
  38.  
  39. EHH.augmentJS();
  40.  
  41. // settings
  42.  
  43. EHH.settings = { };
  44. for (var key in defaults)
  45. EHH.settings[key] = Utils.load(key, defaults[key]);
  46.  
  47. EHH.dontWalk = false;
  48. EHH.onPanda = document.URL.indexOf('e-hentai') == -1;
  49. EHH.defaultColor = EHH.settings[EHH.onPanda ? 'exColor' : 'defaultColor'];
  50. EHH.thumbnails = document.querySelector('.itg .id1') !== null;
  51. EHH.gallery = document.querySelector('#taglist') !== null;
  52.  
  53. // User data
  54. Utils.migrateSettings();
  55. EHH.keywords = Utils.load('keywords',[ ]);
  56. EHH.filters = Utils.load('filters',[ ]);
  57.  
  58. EHH.addMenuItems();
  59. EHH.updateMenu();
  60.  
  61. // Colors
  62.  
  63. var colors =
  64. EHH.onPanda ? { toggle: 'lightblue', toggleHover: 'lightcyan', disable: 'lightgreen',
  65. disableHover: 'springgreen', enable: 'salmon', enableHover: 'lightsalmon',
  66. row1: '#363940', row2: '#4F535B' }
  67. : { toggle: 'slateblue', toggleHover: 'skyblue', disable: 'forestgreen',
  68. disableHover: 'mediumseagreen', enable: 'indianred', enableHover: 'darkred',
  69. row1: '#F2F0E4', row2: '#EDEBDF' };
  70.  
  71. var format = function(text) { return text.replace(/%(\w+)/g,function(x) { return colors[x.slice(1)]; }); };
  72. // Permanent style
  73. var style = document.createElement('style');
  74. style.innerHTML = format(
  75. // popup (general)
  76. '#e-HentaiPopup {' +
  77. 'position: fixed; top: 0; right: 0; padding: 3px; border-radius: 0 !important;' +
  78. 'border: 1px black solid; z-index: 10; margin: 0 !important; min-width: 0 !important; width: auto !important;' +
  79. '}' +
  80. '#e-HentaiPopup:not(:hover) *:not(:first-child) { display: none; }' +
  81. '#e-HentaiPopup * {' +
  82. 'font-family: Verdana, Tahoma, Georgia, Dejavu, "Times New Roman", Serif;' +
  83. 'font-size: 10px;' +
  84. '} #e-Header { text-align: center; position: relative; }' +
  85. '[mode="default"] [mode="settings"], [mode="settings"] [mode="default"] { display: none; }' +
  86. '#e-ToggleMode {' +
  87. 'cursor: pointer; color: %toggle !important; font-weight: bold;' +
  88. 'position: absolute; right: 5px; border-bottom: 1px dotted;' +
  89. '}' +
  90. '#e-ToggleMode:hover { color: %toggleHover !important; }' +
  91. '#e-HentaiPopup div[mode] { width: 350px; text-align: left; }' +
  92. // user menu
  93. '#hideRatedLabel { text-decoration: underline; cursor: pointer; display: none; }' +
  94. '#hideRatedLabel.visible { display: inline; }' +
  95. '#hideRatedLabel.active { color: red; font-weight: bold; }' +
  96. // popup (default view)
  97. '#e-HentaiPopup td:nth-child(2) { text-align: right; }' +
  98. '#e-HentaiPopup td:nth-child(2) a, #e-HentaiPopup tr:last-child a {' +
  99. 'cursor: pointer; font-weight: bold; border-bottom: 1px dotted;' +
  100. '}' +
  101. '#e-HentaiPopup .e-Disable { color: %disable; }' +
  102. '#e-HentaiPopup .e-Disable:hover { color: %disableHover; }' +
  103. '#e-HentaiPopup .e-Enable { color: %enable; }' +
  104. '#e-HentaiPopup .e-Enable:hover { color: %enableHover; }' +
  105. '#e-HentaiPopup tr:last-child a:hover { color: black; }' +
  106. '#e-HentaiPopup td > span { margin-right: 5px; float: right; }' +
  107. '#e-HentaiPopup table { width: 100%; }' +
  108. '#e-HentaiPopup textarea { width: 100%; height: 200px; box-sizing: border-box; -moz-box-sizing: border-box; }' +
  109. // popup (settings view)
  110. '#e-HentaiPopup label { display: block; padding: 2px; }' +
  111. '#e-HentaiPopup input[type="checkbox"] { margin: 0 5px 0 0; float: left; }' +
  112. '[name="slider"]:not([visible="true"]), [name="slider"]:not([visible="true"]) + span { display: none; }' +
  113. '[name="slider"] { margin-left: 20px; width: 250px; }' +
  114. '[name="slider"] + span { position: relative; bottom: 7px; }' +
  115. '#e-HentaiPopup [mode="settings"] { padding: 10px; box-sizing: border-box; -moz-box-sizing: border-box; }' +
  116. '#e-Buttons { text-align: center; padding-top: 20px; }' +
  117. '.e-Button {' +
  118. 'min-width: 100px; height: 25px; line-height: 25px; text-align: center; color: white;' +
  119. 'background: black; display: inline-block; cursor: pointer; margin-right: 10px;' +
  120. '}' +
  121. '.e-Button:hover { text-decoration: underline; }' +
  122. '.e-Button + input { display: none; }' +
  123. '#e-PickerLabel { margin-top: 10px; }' +
  124. '#e-ColorPicker + div { width: 30px; height: 18px; display: inline-block; margin-left: 10px; vertical-align: top; }' +
  125. // highlight/filter style
  126. '.e-Highlighted b { font-weight: inherit; }' +
  127. '.e-Highlighted:not(.e-Transparent), [id^="ta_"][style*="background"] { color: black !important; }' +
  128. '.e-white:not(.e-Transparent) { color: white !important; }' +
  129. '.e-black:not(.e-Transparent) { color: black !important; }' +
  130. '.e-Highlighted:not(.e-Transparent) a { color: inherit !important; }' +
  131. '.e-Highlighted b { font-weight: bold !important; font-size: 115%; text-decoration: underline; }' +
  132. // tag divs
  133. '.e-Tags { position: absolute; top: 0px; left: 0px; text-align: left; color: black; ' +
  134. 'margin-left: 1px; text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;' +
  135. 'font-weight: bold; font-family: "Segoe UI"; font-size: 12px; line-height: 11px;' +
  136. '}' +
  137. '.e-Tags > div { padding: 3px; max-width: 70px; overflow: hidden; transition: max-width .5s linear;' +
  138. 'white-space: nowrap; }' +
  139. '.id3:hover .e-Tags > div { max-width: 200px !important; }' +
  140. '.itg, #pp { display: flex; flex-flow: row wrap; }' +
  141. '.id1 { float: none !important; }'
  142. );
  143. document.head.appendChild(style);
  144.  
  145. // Mutable style
  146. EHH.opaqueFilterCSS = document.createElement('style');
  147. EHH.opaqueFilterCSS.id = 'e-OpaqueFilter';
  148. EHH.opaqueFilterCSSMask = format('{0}#toppane ~ .c, .e-Filtered { display: none !important; }\n' +
  149. '{0}tr.color1 { background: %row1 }\n' +
  150. '{0}tr.color0 { background: %row2 }\n' +
  151. '{1}#toppane ~ .c, .e-Filtered { opacity: {opacity} !important;}\n' +
  152. '{1}.e-Filtered:hover { opacity: 1 !important; -webkit-transition: opacity .1s linear;' +
  153. '-moz-transition: opacity .1s linear; -o-transition: opacity .1s linear; }');
  154. document.head.appendChild(EHH.opaqueFilterCSS);
  155.  
  156. // Popup
  157.  
  158. EHH.generatePopup();
  159.  
  160. // Events
  161. document.addEventListener('DOMNodeInserted',function(e) {
  162. if (e.target.nodeName == 'TBODY')
  163. EHH.walk(e.target);
  164. },false);
  165.  
  166. if (EHH.gallery)
  167. document.addEventListener('DOMNodeInserted',EHH.updateTagList,false);
  168.  
  169. if (!EHH.gallery && !EHH.thumbnails)
  170. EHH.interceptMouseHover();
  171.  
  172. // Data synchronization
  173.  
  174. EHH.link('keywords' , 'keywords' , EHH.updatePopup , EHH.clearRegexes , EHH.walk);
  175. EHH.link('filters' , 'filters' , EHH.updatePopup , EHH.clearRegexes , EHH.walk);
  176. EHH.link('defaultColor' , null , EHH.updatePopup , EHH.walk);
  177.  
  178. EHH.settings.link('defaultColor' , 'defaultColor');
  179. EHH.settings.link('exColor' , 'exColor' );
  180. EHH.settings.link('filterEnabled' , 'filterEnabled' , EHH.updatePopup , EHH.toggleOpacity , EHH.walk);
  181. EHH.settings.link('highlighterEnabled' , 'highlighterEnabled' , EHH.updatePopup , EHH.walk);
  182. EHH.settings.link('opacityEnabled' , 'opacityEnabled' , EHH.updatePopup , EHH.toggleOpacity);
  183. EHH.settings.link('opacity' , 'opacity' , EHH.updatePopup , EHH.toggleOpacity);
  184. EHH.settings.link('showTags' , 'showTags' , EHH.toggleTagDivs);
  185. EHH.settings.link('highlightTags' , 'highlightTags' , EHH.highlightTags);
  186. EHH.settings.link('reorderGalleries' , 'reorderGalleries' , EHH.walk);
  187. EHH.settings.link('hideRatedGalleries' , 'hideRatedGalleries' , EHH.updateMenu, EHH.walk);
  188. EHH.settings.link('showHideOption' , 'showHideOption' , EHH.updateMenu, EHH.walk);
  189. EHH.settings.link('smartForegrounds' , 'smartForegrounds' , EHH.walk);
  190. EHH.settings.link('chronologicalOrder' , 'chronologicalOrder' , EHH.walk);
  191.  
  192. // Start
  193. EHH.toggleOpacity();
  194. EHH.attachListener();
  195. EHH.walk();
  196.  
  197. },
  198. augmentJS: function() {
  199.  
  200. /*Object.getOwnPropertyNames(Array.prototype).forEach(function(x) {
  201. NodeList.prototype[x] = Array.prototype[x];
  202. });*/
  203.  
  204. var linkedObjects = { };
  205. Object.defineProperty(Object.prototype,'link', {
  206. enumerable : false,
  207. configurable : false,
  208. writable : false,
  209. value : function(localProperty,storedProperty,onChangeCallbacks) {
  210. var currentValue = this[localProperty], args = arguments;
  211. var get = function() { return currentValue; };
  212. var set = function(value) {
  213. currentValue = value;
  214. if (storedProperty) Utils.save(storedProperty,currentValue);
  215. for (var i=2;i<args.length;++i) {
  216. if (args[i])
  217. args[i](currentValue);
  218. }
  219. };
  220. delete this[localProperty];
  221. var descriptor = { get: get, set: set, enumerable: true, configurable: true };
  222. Object.defineProperty(this,localProperty,descriptor);
  223. linkedObjects[storedProperty] = { object: this, key: localProperty };
  224. }
  225. });
  226.  
  227. window.addEventListener('storage',function(e) {
  228. if (!linkedObjects.hasOwnProperty(e.key)) return;
  229. var target = linkedObjects[e.key];
  230. target.object[target.key] = JSON.parse(e.newValue);
  231. },false);
  232.  
  233. },
  234.  
  235. addMenuItems: function() {
  236.  
  237. var target = document.querySelector('#searchbox .nopm + .nopm, .nosel + div > form > div + div');
  238. if (!target) return;
  239.  
  240. var label = document.createElement('label');
  241. label.id = 'hideRatedLabel';
  242.  
  243. var input = document.createElement('input');
  244. input.type = 'checkbox';
  245. Utils.linkCheckbox(input,EHH.settings,'hideRatedGalleries');
  246.  
  247. label.appendChild(input);
  248. label.appendChild(document.createTextNode('Hide rated galleries'));
  249. label.setAttribute('title', 'Note that you must use a star color other than yellow in order for this feature to work.');
  250.  
  251. target.appendChild(document.createTextNode('\u00A0'));
  252. target.appendChild(document.createTextNode('\u00A0'));
  253. target.appendChild(label);
  254.  
  255. },
  256.  
  257. updateMenu: function() {
  258.  
  259. EHH.settings.doFilterRated = (EHH.settings.showHideOption && EHH.settings.hideRatedGalleries);
  260.  
  261. if (!document.getElementById('hideRatedLabel')) return;
  262.  
  263. if (!EHH.settings.showHideOption) className = '';
  264. else if (!EHH.settings.hideRatedGalleries) className = 'visible';
  265. else className = 'visible active';
  266.  
  267. document.querySelector('#hideRatedLabel input').checked = EHH.settings.hideRatedGalleries;
  268. document.querySelector('#hideRatedLabel').className = className;
  269. },
  270.  
  271. generatePopup: function() {
  272. EHH.popup = document.createElement('div');
  273. EHH.popup.id = 'e-HentaiPopup';
  274. EHH.popup.className = 'ido';
  275. EHH.popup.setAttribute('mode','default');
  276. EHH.popup.innerHTML =
  277. '<div id="e-Header">' +
  278. '<b>E-H Highlighter</b>' +
  279. '<a id="e-ToggleMode" target="settings">Show settings</a>' +
  280. '</div><hr/>' +
  281. '<div mode="default">' +
  282. '<table align="right">' +
  283. '<tr><td style="text-align:left">Keywords:</td><td><a id="e-HighlighterSwitch">Highlighter: enabled</a></tr>' +
  284. '<tr><td colspan="2"><textarea></textarea></td></tr>' +
  285. '<tr><td style="text-align:left">Filters:</td><td><a id="e-FilterSwitch">Filter: enabled</a></td></tr>' +
  286. '<tr><td colspan="2"><textarea></textarea></td></tr>' +
  287. '<tr><td colspan="2"><a id="e-PopupSave">Save changes</a><span><b>Filtered items:</b> <span id="e-FilteredItems"></span></span></td></tr>' +
  288. '</table>' +
  289. '</div>' +
  290. '<div mode="settings">' +
  291. '<label><input type="checkbox" id="opacitySwitch"> Enable opacity mode for filtered items</label>' +
  292. '<input type="range" name="slider" min="0" max="100"> <span></span>' +
  293. '<label><input type="checkbox" id="tagDivSwitch">Display any tags matching one or more highlight keywords in front of the gallery thumbnails</label>' +
  294. '<label><input type="checkbox" id="highlightTagSwitch">Apply highlighting and filters to each gallery\'s tag list</label>' +
  295. '<label><input type="checkbox" id="chronologicalOrder">Force galleries to appear in chronological upload order (thumbnail mode only)</label>' +
  296. '<label><input type="checkbox" id="reorderGalleriesSwitch">Move highlighted galleries to the top and filtered galleries to the bottom (thumbnail mode only)</label>' +
  297. '<label><input type="checkbox" id="showHideOption">Show "hide rated galleries" button on the search box</label>' +
  298. '<label><input type="checkbox" id="smartForegrounds">Automatically pick the best foreground color (white or black) for highlighted galleries</label>' +
  299. '<label id="e-PickerLabel"><input type="checkbox" style="visibility: hidden">Default highlight color: <input type="color" id="e-ColorPicker"> <div></div></label>' +
  300. '<div id="e-Buttons">' +
  301. '<div class="e-Button" id="e-Export">Export data</div>' +
  302. '<div class="e-Button" id="e-Import">Import data</div><input type="file" accept="application/json">' +
  303. '</div>' +
  304. '</div>';
  305. document.body.appendChild(EHH.popup);
  306. // Popup elements
  307.  
  308. EHH.highlighterSwitch = document.getElementById('e-HighlighterSwitch');
  309. EHH.filterSwitch = document.getElementById('e-FilterSwitch');
  310.  
  311. var textareas = Utils.query('#e-HentaiPopup textarea');
  312. EHH.highlighterArea = textareas[0];
  313. EHH.filterArea = textareas[1];
  314. // Events (default view)
  315.  
  316. Utils.onClick(document.getElementById('e-ToggleMode'),function() {
  317. EHH.popup.setAttribute('mode',this.getAttribute('target'));
  318. var showSettings = this.getAttribute('target') == 'settings';
  319. this.innerHTML = (showSettings ? 'Show keywords' : 'Show settings');
  320. this.setAttribute('target',showSettings ? 'default' : 'settings');
  321. });
  322. [EHH.highlighterSwitch,EHH.filterSwitch].forEach(function(x) {
  323. Utils.onClick(x,function() {
  324. var target = /Highlighter/.test(this.textContent) ? 'highlighterEnabled' : 'filterEnabled';
  325. var status = /enabled/.test(this.textContent);
  326. EHH.settings[target] = !status;
  327. });
  328. });
  329. Utils.onClick(document.getElementById('e-PopupSave'),function() {
  330. var validate = function(regexes) { for (var i=0;i<regexes.length;++i) new RegExp(regexes[i]); };
  331. var keywords = EHH.highlighterArea.value.split(/[\n]/).filter(function(x) { return x.length > 0; });
  332. var filters = EHH.filterArea.value.split(/[\n]/).filter(function(x) { return x.length > 0; });
  333. try {
  334. validate(keywords);
  335. validate(filters);
  336. EHH.dontWalk = true;
  337. EHH.keywords = keywords;
  338. EHH.filters = filters;
  339. EHH.dontWalk = false;
  340. EHH.walk();
  341. } catch (e) {
  342. alert('Couldn\'t parse keyword. ' + e.message + '\nSettings have NOT been saved.');
  343. }
  344. });
  345.  
  346. // Events (settings view)
  347. Utils.linkCheckbox(document.getElementById('opacitySwitch'),EHH.settings,'opacityEnabled');
  348. Utils.linkCheckbox(document.getElementById('tagDivSwitch'),EHH.settings,'showTags');
  349. Utils.linkCheckbox(document.getElementById('highlightTagSwitch'),EHH.settings,'highlightTags');
  350. Utils.linkCheckbox(document.getElementById('reorderGalleriesSwitch'),EHH.settings,'reorderGalleries');
  351. Utils.linkCheckbox(document.getElementById('showHideOption'),EHH.settings,'showHideOption');
  352. Utils.linkCheckbox(document.getElementById('smartForegrounds'),EHH.settings,'smartForegrounds');
  353. Utils.linkCheckbox(document.getElementById('chronologicalOrder'),EHH.settings,'chronologicalOrder');
  354.  
  355. // Opacity slider
  356.  
  357. EHH.slider = EHH.popup.querySelector('[name="slider"]');
  358. if (EHH.slider.type != 'range') EHH.slider = null; // not supported
  359. else {
  360. EHH.slider.value = EHH.settings.opacity * 100;
  361. EHH.slider.nextElementSibling.innerHTML = (Math.floor(EHH.settings.opacity * 10000) / 100) + '%';
  362. EHH.slider.addEventListener('change',function(e) {
  363. e.target.nextElementSibling.innerHTML = e.target.value + '%';
  364. EHH.settings.opacity = parseInt(e.target.value,10) / 100;
  365. },false);
  366. }
  367.  
  368. // Color picker
  369.  
  370. var picker = document.getElementById('e-ColorPicker'),
  371. preview = picker.nextElementSibling,
  372. supported = picker.type == 'color';
  373.  
  374. picker.value = preview.style.backgroundColor = EHH.defaultColor;
  375. if (supported) picker.style.cssText = 'padding: 0px; border: 0px; background: none; position: relative; top: 3px';
  376. else picker.style.cssText = 'width: 60px; color: black';
  377. preview.style.cssText = (supported ? 'display: none;' : 'background-color: ' + EHH.defaultColor);
  378.  
  379. var lastColor = preview.style.backgroundColor;
  380. picker.addEventListener(supported ? 'change' : 'input',function() {
  381. preview.style.backgroundColor = picker.value;
  382. if (preview.style.backgroundColor == lastColor) return;
  383. lastColor = preview.style.backgroundColor;
  384. EHH.settings[EHH.onPanda ? 'exColor' : 'defaultColor'] = picker.value;
  385. EHH.defaultColor = picker.value;
  386. },false);
  387.  
  388. // Import-export functions
  389.  
  390. var importButton = document.getElementById('e-Import'), importInput = importButton.nextElementSibling;
  391. Utils.onClick(importButton,function() { importInput.click(); });
  392. importInput.addEventListener('change',function(e) {
  393. var reader = new FileReader();
  394. reader.onerror = function(e) { alert('Couldn\'t read the selected file.'); };
  395. reader.onload = function(e) {
  396. try {
  397. var data = JSON.parse(this.result);
  398. if (!data.keywords || !data.filters || !data.settings) throw null;
  399. var confirmation = confirm('This will overwrite your data. Are you sure you want to proceed?');
  400. if (confirmation) {
  401. EHH.dontWalk = true;
  402. EHH.keywords = data.keywords;
  403. EHH.filters = data.filters;
  404. for (var key in EHH.settings) EHH.settings[key] = data.settings[key];
  405. setTimeout(function() { window.location.reload(); },50);
  406. }
  407. }
  408. catch (_) { alert('Couldn\'t recognize the selected file.'); }
  409. };
  410. reader.readAsText(this.files[0]);
  411. },false);
  412.  
  413. Utils.onClick(document.getElementById('e-Export'),function() {
  414. var result = { keywords: EHH.keywords, filters: EHH.filters, settings: EHH.settings };
  415. var blob = new Blob([JSON.stringify(result,null,2)],{ type: 'application/json' });
  416. var a = document.createElement('a');
  417. a.href = URL.createObjectURL(blob);
  418. a.download = 'EHH.settings.' + (new Date().valueOf()) + '.json';
  419. document.body.appendChild(a);
  420. a.click();
  421. document.body.removeChild(a);
  422. });
  423.  
  424. EHH.updatePopup();
  425.  
  426. },
  427.  
  428. updatePopup: function() {
  429.  
  430. var updateSwitch = function(target,enable) {
  431. target.textContent = target.textContent.replace(/[^\s]+$/,enable ? 'enabled' : 'disabled');
  432. target.className = enable ? 'e-Disable' : 'e-Enable';
  433. var filtered = document.getElementsByClassName('e-Filtered').length;
  434. document.getElementById('e-FilteredItems').textContent = filtered;
  435. };
  436.  
  437. EHH.highlighterArea.textContent = EHH.keywords.join('\n');
  438. EHH.filterArea.textContent = EHH.filters.join('\n');
  439. updateSwitch(EHH.highlighterSwitch,EHH.settings.highlighterEnabled);
  440. updateSwitch(EHH.filterSwitch,EHH.settings.filterEnabled);
  441.  
  442. document.getElementById('opacitySwitch').checked = EHH.settings.opacityEnabled;
  443.  
  444. },
  445.  
  446. toggleOpacity: function() {
  447. // changes the mutable style to enable or disable opacity
  448. EHH.opaqueFilterCSS.innerHTML = EHH.opaqueFilterCSSMask
  449. .replace(/\{0\}/g,EHH.settings.opacityEnabled ? '//' : '')
  450. .replace(/\{1\}/g,EHH.settings.opacityEnabled ? '' : '//')
  451. .replace(/\{opacity\}/,EHH.settings.opacity);
  452. if (EHH.slider) EHH.slider.setAttribute('visible',EHH.settings.opacityEnabled);
  453. },
  454.  
  455. attachListener: function() {
  456. if (!window || !window.addEventListener) return;
  457. window.addEventListener('message', function(message) {
  458. if (message.data == 'ehWalk')
  459. EHH.walk();
  460. });
  461. },
  462.  
  463. toggleTagDivs: function() {
  464. Utils.query('.e-Tags').forEach(function(x) {
  465. x.style.display = EHH.settings.showTags ? null : 'none';
  466. });
  467. },
  468.  
  469. clearRegexes: function() {
  470. EHH.parse.regexes = null;
  471. },
  472. prepareRegexes: function() {
  473. /* Returns an object containing two properties:
  474. * highlight : an array of keywords; each element is an object with a "type" property ("tag" or "title"),
  475. * a "regex" property, a "color" property (null if no color is specified for that keyword)
  476. * and optional "excludeTitle" and "excludeTags" properties. Keywords will be checked
  477. * sequentially; the first matching keyword with a color specified will decide the item's
  478. * color. If no color is found but the item still has to be highlighted, EHH.defaultColor
  479. * will be used instead
  480. * filters : an object with two properties (tag and title), each one a regular expression to
  481. * be applied to the relevant target
  482. */
  483.  
  484. var splitFilter = function(x,target) {
  485. if (x.length > 0 && x[0] == ':') target.tag.push(x.slice(1));
  486. else if (x.length > 0) target.title.push(x);
  487. };
  488.  
  489. var highlight = EHH.keywords.map(function(x) {
  490. var type = (x[0] == ':' ? 'tag' : 'title');
  491. if (type == 'tag') x = x.slice(1);
  492. var tokens = x.match(/^([^!\/;]+)(![^\/;]+)?(;[^\/]+)?(\/\/?[^\/]+)?$/);
  493. if (!tokens) {
  494. console.error('Unable to parse highlighter "' + x + '"');
  495. return null;
  496. }
  497. var include = tokens[1], excludeTitle = tokens[2], excludeTags = tokens[3], color = tokens[4];
  498. return {
  499. regex: new RegExp(include, 'gi'),
  500. excludeTitle: (excludeTitle && excludeTitle.length > 1 ? new RegExp(excludeTitle.slice(1), 'gi') : null),
  501. excludeTags: (excludeTags && excludeTags.length > 1 ? new RegExp(excludeTags.slice(1), 'gi') : null),
  502. type: type,
  503. color: (color ? color.slice(1) : null)
  504. };
  505. });
  506.  
  507. var filters = { title: [ ], tag: [ ] };
  508. EHH.filters.forEach(function(x) {
  509. if (x[0] == ':') filters.tag.push(x.slice(1));
  510. else filters.title.push(x);
  511. });
  512.  
  513. if (filters.title.length === 0) filters.title.push('EHH: no active title filter');
  514. if (filters.tag.length === 0) filters.tag.push('EHH: no active tag filter');
  515.  
  516. filters.title = new RegExp('(' + filters.title.join('|') + ')','i');
  517. filters.tag = new RegExp('(' + filters.tag.join('|') + ')','i');
  518.  
  519. return { highlight: highlight.filter(function(x) { return x !== null; }), filters: filters };
  520.  
  521. },
  522.  
  523. parse: function(title,tags,rated) {
  524.  
  525. /* title : a string, the title of the gallery item to be parsed
  526. * tags : an array of objects, each one a tag contained in a tag flag
  527. * rated : true if the gallery has been rated, false otherwise
  528. * Uses the object built by EHH.prepareRegexes to decide the course of action for each gallery
  529. * Returns an object with a "result" property indicating what has to be done
  530. * Possible values are "highlighted", "filtered" or null for no action
  531. * Filters take precedence over highlight keywords
  532. * If the gallery item is to be highlighted, the result will also contain three additional properties:
  533. * - titleKeywords : a list of title substrings that match one or more keywords (can be empty)
  534. * - tagKeywords : a list of matching tags (to be passed to EHH.addTagDiv if EHH.settings.showTags is set, can be empty)
  535. * - color : the color the gallery needs to be highlighted in (EHH.defaultColor if the user did not specify any color)
  536. */
  537.  
  538. if (!EHH.parse.regexes)
  539. EHH.parse.regexes = EHH.prepareRegexes();
  540.  
  541. var regexes = EHH.parse.regexes;
  542.  
  543. if (EHH.settings.doFilterRated && rated)
  544. return { result: 'filtered' };
  545.  
  546. if (EHH.settings.filterEnabled) {
  547. var filtered = regexes.filters.title.test(title) || tags.some(function(x) { return regexes.filters.tag.test(x.tag); });
  548. if (filtered) return { result: 'filtered' };
  549. }
  550.  
  551. if (EHH.settings.highlighterEnabled) {
  552. var titleKeywords = { }, tagKeywords = { }, color = null;
  553. regexes.highlight.forEach(function(data) {
  554. // apply title exclusion
  555. if (data.excludeTitle !== null && title.match(data.excludeTitle)) return;
  556. // apply tag highlights + tag exclusion
  557. if (data.type == 'tag' || data.excludeTags !== null) {
  558. var excluded = (data.excludeTags !== null && tags.some(function(tag) { return !!tag.tag.match(data.excludeTags); }));
  559. if (!excluded) {
  560. tags.forEach(function(tag) {
  561. if (tagKeywords.hasOwnProperty(tag.tag) || !tag.tag.match(data.regex)) return;
  562. tagKeywords[tag.tag] = tag.color;
  563. if (!color) color = data.color;
  564. });
  565. }
  566. }
  567. // apply title highlights
  568. if (data.type == 'title' && !excluded) {
  569. var tokens = title.match(data.regex);
  570. if (!tokens) return;
  571. tokens = tokens.length == 1 ? tokens : tokens.slice(1);
  572. for (var i=0;i<tokens.length;++i) titleKeywords[tokens[i]] = true;
  573. if (!color) color = data.color;
  574. }
  575. });
  576.  
  577. titleKeywords = Object.keys(titleKeywords);
  578. if (titleKeywords.length === 0 && Object.keys(tagKeywords).length === 0) return { result: null };
  579. return { result: 'highlighted', titleKeywords: titleKeywords, tagKeywords: tagKeywords, color: color || EHH.defaultColor };
  580. }
  581.  
  582. return { result: null };
  583.  
  584. },
  585.  
  586. computeTagColor: function(tag) {
  587. // don't use style.backgroundPositionY
  588. var y = parseInt(tag.style.cssText.split(/\s/).slice(-1)[0],10);
  589. y = -(y+1) / 17;
  590. return ['salmon','darkorange','gold','mediumaquamarine','skyblue','mediumorchid'][y];
  591. },
  592.  
  593. // extracts both the title and a list of tags for a given target
  594. // tags are parsed into objects with "tag" and "color" properties representing
  595. // respectively the tag itself and the color of their associated tag flag
  596. extractData: function(target) {
  597.  
  598. if (!target) return null;
  599.  
  600. var title = target.querySelector('.it5 > a, .id2 > a, [class^="t2"] > a') || target.querySelector('.itd a');
  601. if (!title && target.className.indexOf('t2') === 0) title = target.firstChild;
  602. if (!title) return null;
  603.  
  604. var tags = [ ];
  605. if (target.hasAttribute('eh-tags')) {
  606. var tagList = (target.getAttribute('eh-tags') || '').split(/,/);
  607. tags = tagList
  608. .map(function(x) { return x.trim(); })
  609. .filter(function(x) { return x.length > 0; })
  610. .map(function(x) { return { tag: x, color: 'salmon' }; });
  611. } else {
  612. var tagElements = Utils.query(target,'.tft, .tfl');
  613. tags = tagElements.map(function(x) {
  614. var color = EHH.computeTagColor(x);
  615. return x.title.split(/,/).map(function(tag) {
  616. return { tag: tag, color: color };
  617. });
  618. });
  619. }
  620.  
  621. // flatten list if necessary
  622. tags = [].concat.apply([], tags);
  623.  
  624. var rated = !!target.querySelector('.irb, .irg, .irr');
  625.  
  626. return { title: title, tags: tags, rated: rated };
  627.  
  628. },
  629.  
  630. addTagDiv: function(target,tags) {
  631. if (!tags || Object.keys(tags).length === 0) return;
  632. var div = document.createElement('div');
  633. div.className = 'e-Tags';
  634. var html = '';
  635. for (t in tags) {
  636. var tag = t.replace(/^.+:/,''), color = tags[t];
  637. html += '<div style="background-color: ' + color + '">' + tag + '</div>';
  638. }
  639. div.innerHTML = html;
  640. if (!EHH.settings.showTags) div.style.display = 'none';
  641. var temp = target.getElementsByClassName('id3')[0];
  642. if (temp) temp.firstElementChild.appendChild(div);
  643. else target.appendChild(div);
  644. },
  645. walk: function(root) {
  646.  
  647. /* walks the DOM to highlight and filter gallery items (or the taglist) */
  648.  
  649. if (EHH.gallery) {
  650. EHH.highlightTags();
  651. return;
  652. }
  653.  
  654. if (EHH.dontWalk) return;
  655.  
  656. var removeTagDiv = function(target) {
  657. Utils.query(target,'.e-Tags').forEach(function(x) { x.parentNode.removeChild(x); });
  658. };
  659.  
  660. var editTitle = function(target,keywords) {
  661. var temp = target.innerHTML;
  662. keywords.forEach(function(keyword) {
  663. var length = keyword.length, n = target.innerHTML.indexOf(keyword);
  664. while (n != -1) {
  665. temp = temp.slice(0,n) + new Array(length+1).join('\0') + temp.slice(n+length);
  666. n = target.innerHTML.indexOf(keyword,n+1);
  667. }
  668. });
  669. return temp.replace(/\0+/g,function(match,start) {
  670. return '<b>' + target.innerHTML.slice(start,start+match.length) + '</b>';
  671. });
  672. };
  673.  
  674. // ----------
  675. var flip = 1,
  676. order = 1,
  677. targets = Utils.query('[class^="gtr"], [class^="id1"], div[class^="t2"]'),
  678. groups = (EHH.settings.reorderGalleries && EHH.thumbnails ? [ [ ], [ ], [ ] ] : null);
  679.  
  680. if (EHH.settings.chronologicalOrder && EHH.thumbnails) {
  681. var ids = targets.map(function(x, n) {
  682. var target = x.querySelector('.id3 > a');
  683. if (!target) return [ n, 2**63 + n ];
  684. var tokens = target.getAttribute('href').match(/\/g\/(\d+)/);
  685. return [ n, (tokens ? parseInt(tokens[1], 10) : 2**63 + n) ];
  686. });
  687. ids.sort(function(a,b) { return b[1] - a[1]; })
  688. targets = ids.map(function(x) { return targets[x[0]]; });
  689. }
  690.  
  691. targets.forEach(function(target) {
  692.  
  693. var data = EHH.extractData(target);
  694. if (data === null) return;
  695.  
  696. // reset element
  697. target.className = target.className.replace(/\s?e-\w+/g,'');
  698. target.style.cssText = target.style.cssText.replace(/(background-color|order|color):.+?;/g,'');
  699. data.title.innerHTML = data.title.innerHTML = data.title.innerHTML.replace(/<\/?b>/g,'');
  700. removeTagDiv(target);
  701.  
  702. var parsed = EHH.parse(data.title.textContent,data.tags,data.rated);
  703.  
  704. if (parsed.result == 'filtered') {
  705. target.className += ' e-Filtered';
  706. if (groups !== null) groups[2].push(target);
  707. }
  708.  
  709. else if (parsed.result == 'highlighted') {
  710. if (parsed.color[0] != '/') { // background color
  711. if (parsed.color != 'transparent') {
  712. target.className += ' e-Highlighted';
  713. target.style.cssText += 'background-color: ' + parsed.color + ' !important;';
  714. if (EHH.settings.smartForegrounds) {
  715. var bestForeground = EHH.getBestForeground(parsed.color);
  716. target.className += ' e-' + bestForeground;
  717. }
  718. } else {
  719. target.className += ' e-Highlighted e-Transparent';
  720. }
  721. } else { // foreground color
  722. target.className += ' e-Highlighted';
  723. target.style.cssText += 'color: ' + parsed.color.slice(1) + ' !important;';
  724. }
  725. data.title.innerHTML = editTitle(data.title,parsed.titleKeywords);
  726. if (EHH.thumbnails) EHH.addTagDiv(target,parsed.tagKeywords);
  727. if (groups !== null) groups[0].push(target);
  728. }
  729.  
  730. else if (groups !== null)
  731. groups[1].push(target);
  732.  
  733. if (!/^gtr/.test(target.className)) return;
  734. if (parsed.result == 'filtered' && !EHH.opaque) return;
  735. flip = (flip+1)%2;
  736. if (target.className.indexOf('color') == -1) target.className += ' color' + flip;
  737. else target.className = target.className.replace(/color\d/,'color' + flip);
  738.  
  739. });
  740.  
  741. if (groups !== null) {
  742. Array.prototype.concat.apply([ ],groups).forEach(function(g) {
  743. g.style.cssText += 'order: ' + (order++) + ';';
  744. });
  745. };
  746.  
  747. targets.forEach(function(target) {
  748. if (!target.style.order)
  749. target.style.cssText += 'order: ' + (order++) + ';';
  750. });
  751. var filtered = document.getElementsByClassName('e-Filtered').length;
  752. document.getElementById('e-FilteredItems').textContent = filtered;
  753.  
  754. },
  755.  
  756. highlightTags: function() {
  757. Utils.query('[id^="ta_"]').forEach(function(x) {
  758. if (!EHH.settings.highlightTags) x.style.cssText = '';
  759. else {
  760. var fullName = x.id.slice(3).replace(/_/g,' ');
  761. var data = EHH.parse(fullName,[{ tag: fullName, color: null }]);
  762. // if no match is found and the tag has an alias, re-run the check on the alias
  763. if (data.result === null && fullName && x.textContent.indexOf(' | ') > -1) {
  764. var namespace = fullName.indexOf(':') === 0 ? '' : fullName.slice(0, fullName.indexOf(':')) + ':';
  765. var alias = x.textContent.slice(x.textContent.indexOf(' | ') + 3);
  766. fullName = namespace + alias;
  767. data = EHH.parse(fullName, [{ tag: fullName, color: null }]);
  768. }
  769. x.style.cssText = data.result == 'filtered' ? 'text-decoration: line-through' :
  770. data.result == 'highlighted' ? 'background-color: ' + data.color.replace(/^\//, '') + ' !important' :
  771. '';
  772. }
  773. });
  774. },
  775.  
  776. updateTagList: function(e) {
  777. if (e.target.nodeName != 'TABLE' || e.target.parentNode.id != 'taglist') return;
  778. if (EHH.settings.highlightTags) EHH.highlightTags();
  779. },
  780.  
  781. interceptMouseHover: function(e) {
  782. if (typeof(MutationObserver) != 'function') return;
  783. var observer = new MutationObserver(function(e) {
  784. var thumbnail = e[0].target;
  785. if (thumbnail.style.visibility == 'hidden')
  786. Utils.query(thumbnail,'.e-Tags').forEach(function(x) { x.parentNode.removeChild(x); });
  787. else if (EHH.settings.showTags) {
  788. var row = document.evaluate('ancestor::tr[1]',thumbnail,null,9,null).singleNodeValue,
  789. data = EHH.extractData(row);
  790. if (data === null) return;
  791. parsed = EHH.parse(data.title.textContent,data.tags);
  792. if (parsed.result == 'highlighted') EHH.addTagDiv(thumbnail,parsed.tagKeywords);
  793. }
  794. });
  795. Utils.query('.it2[id^="i"]').forEach(function(x) {
  796. observer.observe(x,{ attributes: true });
  797. });
  798. },
  799.  
  800. getBestForeground: function(color) {
  801. if (!EHH.colorCache) EHH.colorCache = { };
  802. if (EHH.colorCache.hasOwnProperty(color)) return EHH.colorCache[color];
  803. var parsedColor = null, div = null;
  804. try {
  805. div = document.createElement('div');
  806. div.style.color = color;
  807. document.body.appendChild(div);
  808. parsedColor = window.getComputedStyle(div).color.match(/(\d+)/g);
  809. parsedColor = [ parseInt(parsedColor[0], 10), parseInt(parsedColor[1], 10), parseInt(parsedColor[2], 10) ];
  810. } catch (e) {
  811. parsedColor = [ 255, 255, 255 ];
  812. } finally {
  813. if (div && div.parentNode)
  814. div.parentNode.removeChild(div);
  815. }
  816. var whiteDistance = Math.sqrt(Math.pow(255 - parsedColor[0], 2) + Math.pow(255 - parsedColor[1], 2) + Math.pow(255 - parsedColor[2], 2));
  817. var blackDistance = Math.sqrt(Math.pow(-parsedColor[0], 2) + Math.pow(-parsedColor[1], 2) + Math.pow(-parsedColor[2], 2));
  818. var result = (whiteDistance > blackDistance ? 'white' : 'black');
  819. EHH.colorCache[color] = result;
  820. return result;
  821. }
  822.  
  823. };
  824.  
  825. var Utils = {
  826.  
  827. // moves localStorage-based settings to GM_[gs]etValue when possible
  828. migrateSettings: function() {
  829. if (typeof(GM_getValue) == 'undefined' || typeof(GM_setValue) == 'undefined') return;
  830. if (Utils.load('migrationDone2', false)) return;
  831. try {
  832. for (var key in EHH.settings) {
  833. if (localStorage.getItem(key) === null) continue;
  834. EHH.settings[key] = JSON.parse(localStorage.getItem(key));
  835. Utils.save(key, EHH.settings[key]);
  836. }
  837. ['keywords', 'filters'].forEach(function(key) {
  838. if (localStorage.getItem(key) === null) return;
  839. EHH[key] = JSON.parse(localStorage.getItem(key));
  840. Utils.save(key,EHH[key]);
  841. });
  842. Utils.save('migrationDone2', true);
  843. } catch (e) { }
  844. },
  845.  
  846.  
  847. save: function(key,value) {
  848. if (typeof(GM_setValue) != 'undefined') GM_setValue(key, JSON.stringify(value));
  849. else localStorage.setItem(key, JSON.stringify(value));
  850. },
  851.  
  852. load: function(key,def) {
  853. var result = null;
  854. if (typeof(GM_getValue) != 'undefined') result = GM_getValue(key, null);
  855. else result = (localStorage.getItem(key) || null);
  856. return (result === null ? def : JSON.parse(result));
  857. },
  858.  
  859. onClick: function(element,f) {
  860. element.addEventListener('click',function(e) {
  861. if (e.which != 1) return;
  862. if (f) f.call(this);
  863. },false);
  864. },
  865.  
  866. linkCheckbox: function(checkbox,object,property) {
  867. if (!object) return;
  868. if (object.hasOwnProperty(property)) checkbox.checked = object[property];
  869. Utils.onClick(checkbox,function() { object[property] = checkbox.checked; });
  870. },
  871.  
  872. query: function(root,selector) {
  873. if (!selector)
  874. return Array.prototype.slice.call(document.querySelectorAll(root),0);
  875. else
  876. return Array.prototype.slice.call(root.querySelectorAll(selector),0);
  877. }
  878. };
  879.  
  880. EHH.init();