E-Hentai Highlighter-Mod

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

ของเมื่อวันที่ 17-10-2015 ดู เวอร์ชันล่าสุด

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