· 8 years ago · Jan 04, 2018, 03:10 PM
1// ==UserScript==
2// @name HIT Scraper WITH EXPORT (CHRISTURK TEMPORARY FIX, UNINSTALL AFTER FEIH FIXES ORIGINAL VERSION)
3// @author feihtality + bugfixes
4// @description Snag HITs. mturk.
5// @namespace https://greasyfork.org/en/users/12709
6// @include /^https://w(ww|orker).mturk.com/.*hit[-_]?scraper$/
7// @version 4.1.3
8// @grant none
9// ==/UserScript==
10
11(function() {
12 'use strict';
13
14 const ENV = Object.freeze({
15 LEGACY : 'www.mturk.com',
16 NEXT : 'worker.mturk.com',
17 HOST : window.location.hostname,
18 ORIGIN : window.location.origin,
19 ISFF : Boolean(window.sidebar),
20 VERSION: '4.1.3'
21 });
22 const URL_SELF = 'https://greasyfork.org/en/scripts/10615-hit-scraper-with-export#ugTop';
23 const DOC_TITLE = 'HIT Scraper';
24 const TO_BASE = 'https://turkopticon.ucsd.edu/';
25 const TO_REPORTS = TO_BASE + 'reports?id=';
26 const TO_API = TO_BASE + 'api/multi-attrs.php?ids=';
27
28 const ico = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wsTDwctGwJAtQAAAYRJREFUOMudkz1rWmEUx3/6gFeC1A5p6RJByGTGTpEM3iHfwMmxfggH1yyB69BFyFAy5FIQ6wdw9CVxUOhQXFykYFcTLvEaiuafQU0wXoPkwIHnOS8/znPOeWAh18AU0I7qA61lLjfbAl3X1Ww2k+u620AtgP9BTsdx1O/3lUqlNBgM5DhOEGAaWh7eLeEgYywWo1QqMRwO8X2fdrtNsVjcCtkorVKpSJKy2awsy1Imk1Gv19vWh02j53mSpHQ6vctENo2dTkcrGY/HqlarSiaTuwMSiYRqtZomk8kzqNvt7g5YqTFGtm1LkjzPC4wJnEKz2cS2bSKRCMYYAOr1+htTCFtr1EKhoEajId/3NRqNVC6XFY/HF/69LyKfE9bivlikfA72D+H8DHh8e3PyOfAMfJjDj58YoMjvP4bQPVz9g4MU7Ifh7y2EDJgoHB/BqQ3fLuDzAdz14fIXaP6w+okvT8jnRFub+r0mTr6+bmJzVVgLmKw5w1ER/SQiH4O6fw80AJ4AC20qRDxx28IAAAAASUVORK5CYII=',
29 audio0 = 'T2dnUwACAAAAAAAAAAB8mpoRAAAAAFLKt9gBHgF2b3JiaXMAAAAAARErAAAAAAAAkGUAAAAAAACZAU9nZ1MAAAAAAAAAAAAAfJqaEQEAAACHYsq6Cy3///////////+1A3ZvcmJpcx0AAABYaXBoLk9yZyBsaWJWb3JiaXMgSSAyMDA1MDMwNAAAAAABBXZvcmJpcxJCQ1YBAAABAAxSFCElGVNKYwiVUlIpBR1jUFtHHWPUOUYhZBBTiEkZpXtPKpVYSsgRUlgpRR1TTFNJlVKWKUUdYxRTSCFT1jFloXMUS4ZJCSVsTa50FkvomWOWMUYdY85aSp1j1jFFHWNSUkmhcxg6ZiVkFDpGxehifDA6laJCKL7H3lLpLYWKW4q91xpT6y2EGEtpwQhhc+211dxKasUYY4wxxsXiUyiC0JBVAAABAABABAFCQ1YBAAoAAMJQDEVRgNCQVQBABgCAABRFcRTHcRxHkiTLAkJDVgEAQAAAAgAAKI7hKJIjSZJkWZZlWZameZaouaov+64u667t6roOhIasBADIAAAYhiGH3knMkFOQSSYpVcw5CKH1DjnlFGTSUsaYYoxRzpBTDDEFMYbQKYUQ1E45pQwiCENInWTOIEs96OBi5zgQGrIiAIgCAACMQYwhxpBzDEoGIXKOScggRM45KZ2UTEoorbSWSQktldYi55yUTkompbQWUsuklNZCKwUAAAQ4AAAEWAiFhqwIAKIAABCDkFJIKcSUYk4xh5RSjinHkFLMOcWYcowx6CBUzDHIHIRIKcUYc0455iBkDCrmHIQMMgEAAAEOAAABFkKhISsCgDgBAIMkaZqlaaJoaZooeqaoqqIoqqrleabpmaaqeqKpqqaquq6pqq5seZ5peqaoqp4pqqqpqq5rqqrriqpqy6ar2rbpqrbsyrJuu7Ks256qyrapurJuqq5tu7Js664s27rkearqmabreqbpuqrr2rLqurLtmabriqor26bryrLryratyrKua6bpuqKr2q6purLtyq5tu7Ks+6br6rbqyrquyrLu27au+7KtC7vourauyq6uq7Ks67It67Zs20LJ81TVM03X9UzTdVXXtW3VdW1bM03XNV1XlkXVdWXVlXVddWVb90zTdU1XlWXTVWVZlWXddmVXl0XXtW1Vln1ddWVfl23d92VZ133TdXVblWXbV2VZ92Vd94VZt33dU1VbN11X103X1X1b131htm3fF11X11XZ1oVVlnXf1n1lmHWdMLqurqu27OuqLOu+ruvGMOu6MKy6bfyurQvDq+vGseu+rty+j2rbvvDqtjG8um4cu7Abv+37xrGpqm2brqvrpivrumzrvm/runGMrqvrqiz7uurKvm/ruvDrvi8Mo+vquirLurDasq/Lui4Mu64bw2rbwu7aunDMsi4Mt+8rx68LQ9W2heHVdaOr28ZvC8PSN3a+AACAAQcAgAATykChISsCgDgBAAYhCBVjECrGIIQQUgohpFQxBiFjDkrGHJQQSkkhlNIqxiBkjknIHJMQSmiplNBKKKWlUEpLoZTWUmotptRaDKG0FEpprZTSWmopttRSbBVjEDLnpGSOSSiltFZKaSlzTErGoKQOQiqlpNJKSa1lzknJoKPSOUippNJSSam1UEproZTWSkqxpdJKba3FGkppLaTSWkmptdRSba21WiPGIGSMQcmck1JKSamU0lrmnJQOOiqZg5JKKamVklKsmJPSQSglg4xKSaW1kkoroZTWSkqxhVJaa63VmFJLNZSSWkmpxVBKa621GlMrNYVQUgultBZKaa21VmtqLbZQQmuhpBZLKjG1FmNtrcUYSmmtpBJbKanFFluNrbVYU0s1lpJibK3V2EotOdZaa0ot1tJSjK21mFtMucVYaw0ltBZKaa2U0lpKrcXWWq2hlNZKKrGVklpsrdXYWow1lNJiKSm1kEpsrbVYW2w1ppZibLHVWFKLMcZYc0u11ZRai621WEsrNcYYa2415VIAAMCAAwBAgAlloNCQlQBAFAAAYAxjjEFoFHLMOSmNUs45JyVzDkIIKWXOQQghpc45CKW01DkHoZSUQikppRRbKCWl1losAACgwAEAIMAGTYnFAQoNWQkARAEAIMYoxRiExiClGIPQGKMUYxAqpRhzDkKlFGPOQcgYc85BKRljzkEnJYQQQimlhBBCKKWUAgAAChwAAAJs0JRYHKDQkBUBQBQAAGAMYgwxhiB0UjopEYRMSielkRJaCylllkqKJcbMWomtxNhICa2F1jJrJcbSYkatxFhiKgAA7MABAOzAQig0ZCUAkAcAQBijFGPOOWcQYsw5CCE0CDHmHIQQKsaccw5CCBVjzjkHIYTOOecghBBC55xzEEIIoYMQQgillNJBCCGEUkrpIIQQQimldBBCCKGUUgoAACpwAAAIsFFkc4KRoEJDVgIAeQAAgDFKOSclpUYpxiCkFFujFGMQUmqtYgxCSq3FWDEGIaXWYuwgpNRajLV2EFJqLcZaQ0qtxVhrziGl1mKsNdfUWoy15tx7ai3GWnPOuQAA3AUHALADG0U2JxgJKjRkJQCQBwBAIKQUY4w5h5RijDHnnENKMcaYc84pxhhzzjnnFGOMOeecc4wx55xzzjnGmHPOOeecc84556CDkDnnnHPQQeicc845CCF0zjnnHIQQCgAAKnAAAAiwUWRzgpGgQkNWAgDhAACAMZRSSimllFJKqKOUUkoppZRSAiGllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimVUkoppZRSSimllFJKKaUAIN8KBwD/BxtnWEk6KxwNLjRkJQAQDgAAGMMYhIw5JyWlhjEIpXROSkklNYxBKKVzElJKKYPQWmqlpNJSShmElGILIZWUWgqltFZrKam1lFIoKcUaS0qppdYy5ySkklpLrbaYOQelpNZaaq3FEEJKsbXWUmuxdVJSSa211lptLaSUWmstxtZibCWlllprqcXWWkyptRZbSy3G1mJLrcXYYosxxhoLAOBucACASLBxhpWks8LR4EJDVgIAIQEABDJKOeecgxBCCCFSijHnoIMQQgghREox5pyDEEIIIYSMMecghBBCCKGUkDHmHIQQQgghhFI65yCEUEoJpZRSSucchBBCCKWUUkoJIYQQQiillFJKKSGEEEoppZRSSiklhBBCKKWUUkoppYQQQiillFJKKaWUEEIopZRSSimllBJCCKGUUkoppZRSQgillFJKKaWUUkooIYRSSimllFJKCSWUUkoppZRSSikhlFJKKaWUUkoppQAAgAMHAIAAI+gko8oibDThwgMQAAAAAgACTACBAYKCUQgChBEIAAAAAAAIAPgAAEgKgIiIaOYMDhASFBYYGhweICIkAAAAAAAAAAAAAAAABE9nZ1MABAgkAAAAAAAAfJqaEQIAAAB89IOyJjhEQUNNRE5TRENHS0xTRllHSEpISUdORk1GSEdISUNHP0ZHS1IhquPYHv5OAgC/7wFATp2pUBdX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',
30 /*'*/
31 audio1 = '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VVVVVVVVVVVVVQBqs0jMnBWmgKng1FRU32lmZmb+N6/V+w+MygnAXTBSU3/Ffjw4KGwvhYijgbg038TRQL8U3//xv5cO4FZf/z8bdj8V3CpuILkwNl5P8KE+53G3eCoQUAIQxdpeAGCps5MdqtGpNehUFBQFS2qx1bDEzNSDATAKKlIBjsJGEQBFZnmMUrStzKW+pXKVDOZ+hnQ2jloarUNK2iGVjPlUVcHRF4NCI9e2HcNCJ4NQ0eEQlnbmh0FYaUxBTUUzLjk5LjNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kkScD/K0GL+QyBnCYEgHsjBlXAAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=';
32
33 let scraperHistory;
34 const defaults = { //{{{
35 themes : {//{{{
36 whisper : {
37 highlight : '#1F3847',
38 background : '#232A2F',
39 accent : '#00ffff',
40 bodytable : '#AFCCDE',
41 cpBackground: '#394752',
42 toHigh : '#009DFF',
43 toGood : '#40B6FF',
44 toAverage : '#7ACCFF',
45 toLow : '#B5E3FF',
46 toPoor : '#DEF1FC',
47 hitDB : '#CADA95',
48 nohitDB : '#DA95A8',
49 unqualified : '#808080',
50 reqmaster : '#C1E1F6',
51 nomaster : '#D6C1F6',
52 defaultText : '#AFCCDE',
53 inputText : '#98D6D6',
54 secondText : '#808080',
55 link : '#003759',
56 vlink : '#40F0F0',
57 toNone : '#AFCCDE',
58 export : '#86939C',
59 hover : '#1E303B'
60 },
61 solDark : {
62 highlight : '#657b83',
63 background : '#002b36',
64 accent : '#b58900',
65 bodytable : '#839496',
66 cpBackground: '#073642',
67 toHigh : '#859900',
68 toGood : '#A2BA00',
69 toAverage : '#b58900',
70 toLow : '#cb4b16',
71 toPoor : '#dc322f',
72 hitDB : '#82D336',
73 nohitDB : '#D33682',
74 unqualified : '#9F9F9F',
75 reqmaster : '#B58900',
76 nomaster : '#839496',
77 defaultText : '#839496',
78 inputText : '#eee8d5',
79 secondText : '#93a1a1',
80 link : '#000000',
81 vlink : '#6c71c4',
82 toNone : '#839496',
83 export : '#CCC6B4',
84 hover : '#122A30'
85 },
86 solLight: {
87 highlight : '#657b83',
88 background : '#fdf6e3',
89 accent : '#b58900',
90 bodytable : '#657b83',
91 cpBackground: '#eee8d5',
92 toHigh : '#859900',
93 toGood : '#A2BA00',
94 toAverage : '#b58900',
95 toLow : '#cb4b16',
96 toPoor : '#dc322f',
97 hitDB : '#82D336',
98 nohitDB : '#36D0D3',
99 unqualified : '#9F9F9F',
100 reqmaster : '#B58900',
101 nomaster : '#6C71C4',
102 defaultText : '#657b83',
103 inputText : '#6FA3A3',
104 secondText : '#A6BABA',
105 link : '#000000',
106 vlink : '#6c71c4',
107 toNone : '#657b83',
108 export : '#000000',
109 hover : '#C7D2D6'
110 },
111 classic : {
112 highlight : '#30302F',
113 background : '#131313',
114 accent : '#94704D',
115 bodytable : '#000000',
116 cpBackground: '#131313',
117 toHigh : '#66CC66',
118 toGood : '#ADFF2F',
119 toAverage : '#FFD700',
120 toLow : '#FF9900',
121 toPoor : '#FF3030',
122 hitDB : '#66CC66',
123 nohitDB : '#FF3030',
124 unqualified : '#9F9F9F',
125 reqmaster : '#551A8B',
126 nomaster : '#0066CC',
127 defaultText : '#94704D',
128 inputText : '#000000',
129 secondText : '#997553',
130 link : '#0000FF',
131 vlink : '#800080',
132 toNone : '#d3d3d3',
133 export : '#000000',
134 hover : '#21211F'
135 },
136 deluge : {
137 highlight : '#1F3847',
138 background : '#434e56',
139 accent : '#fbde2d',
140 bodytable : '#f8f8f8',
141 cpBackground: '#384147',
142 toHigh : '#6FFA3C',
143 toGood : '#D9FC35',
144 toAverage : '#fbde2d',
145 toLow : '#FAB050',
146 toPoor : '#FA6F50',
147 hitDB : '#d8fa3c',
148 nohitDB : '#DA95A8',
149 unqualified : '#ADC6EE',
150 reqmaster : '#BFADEE',
151 nomaster : '#ADEEDF',
152 defaultText : '#f8f8f8',
153 inputText : '#D8FA3C',
154 secondText : '#ADC6EE',
155 link : '#99004F',
156 vlink : '#DCEEAD',
157 toNone : '#97A167',
158 export : '#ADC6EE',
159 hover : '#426075'
160 }
161 },//}}}
162 vbTemplate: '[table][tr][td][b]Title:[/b] [URL=${previewLink}]${title}[/URL] | [URL=${pandaLink}]PANDA[/URL]\n' +
163 '[b]Requester:[/b] [URL=${requesterLink}]${requesterName}[/URL] [${requesterId}] ' +
164 '([URL=' + TO_REPORTS + '${requesterId}]TO[/URL])\n' +
165 '[b]TO Ratings:[/b]\n${toVerbose}\n${toFoot}\n' +
166 '[b]Description:[/b] ${description}\n[b]Time:[/b] ${time}\n[b]HITs Available:[/b] ${numHits}\n' +
167 '[b]Reward:[/b] [COLOR=green][b]${reward}[/b][/COLOR]\n' +
168 '[b]Qualifications:[/b] ${quals}[/td][/tr][/table]'
169 },//}}}
170
171 Settings = {//{{{
172 defaults : {//{{{
173 themes : { name: 'classic', colors: defaults.themes },
174 colorType : 'sim',
175 sortType : 'adj',
176 toWeights : { comm: '1', pay: '3', fair: '3', fast: '1' },
177 exportVb : true,
178 exportIrc : true,
179 exportHwtf : true,
180 notifySound : [false, 'ding'],
181 notifyBlink : false,
182 notifyTaskbar : false,
183 volume : { ding: 1, squee: 1 },
184 wildblocks : true,
185 showCheckboxes: true,
186 hitColor : 'link',
187 fontSize : 11,
188 shineOffset : 1,
189
190 refresh : '0',
191 pages : '1',
192 skips : false,
193 resultsPerPage: '50',
194 batch : '',
195 pay : '',
196 qual : true,
197 monly : false,
198 mhide : false,
199 searchBy : 0,
200 invert : false,
201 shine : '300',
202 minTOPay : '',
203 hideNoTO : false,
204 onlyViable : false,
205 disableTO : false,
206 sortPay : false,
207 sortAll : false,
208 search : '',
209 hideBlock : true,
210 onlyIncludes : false,
211 shineInc : true,
212 sortAsc : false,
213 sortDsc : true,
214 gbatch : false,
215 bubbleNew : false,
216
217 vbTemplate: defaults.vbTemplate,
218 vbSym : '\u2605' // star
219 },//}}}
220 user: {}, save: function() { localStorage.setItem('scraper_settings', JSON.stringify(this.user)); },
221 draw : function() {//{{{
222 var
223 _ccs = 'https://greasyfork.org/en/scripts/3118-mmmturkeybacon-color-coded-search-with-checkpoints',
224 _hwtf = 'https://www.reddit.com/r/HITsWorthTurkingFor',
225 _general = //{{{
226 `<div>
227 <div style="float:left; margin-left:15px">
228 <span style="position:relative; left:-8px"><b>Export Buttons</b></span>
229 <p><label for="exportVb" style="float:left; width:51px">vBulletin</label>
230 <input id="exportVb" name="export" value="vb" type="checkbox" ${this.user.exportVb ? 'checked' : ''}/></p>
231 <p><label for="exportIrc" style="float:left; width:51px">IRC</label>
232 <input id="exportIrc" name="export" value="irc" type="checkbox" ${this.user.exportIrc ? 'checked' : ''}/></p>
233 <p><label for="exportHwtf" style="float:left; width:51px">Reddit</label>
234 <input id="exportHwtf" name="export" value="hwtf" type="checkbox" ${this.user.exportHwtf ? 'checked' : ''}/></p>
235 </div>
236 <section style="margin-left:110px">
237 <span style="position:relative; left:10px"><i>vBulletin</i></span><br>
238 Show a button in the results to export the specified HIT with vBulletin formatted text to share on forums.
239 </section><section style="margin-left:110px">
240 <span style="position:relative; left:10px"><i>IRC</i></span><br>
241 Show a button in the results to export the specified HIT streamlined for sharing on IRC.
242 </section><section style="margin-left:110px">
243 <span style="position:relative; left:10px"><i>Reddit</i></span><br>
244 Show a button in the results to export the specified HIT for sharing on Reddit, formatted to
245 <a style="color:black" href="${_hwtf}" target="_blank">r/HITsWorthTurkingFor</a> standards.
246 </section>
247 </div><div>
248 <div style="float:left; margin-left:15px">
249 <span style="position:relative; left:-8px"><b>Bubble New HITs</b></span>
250 <p><label for="bubbleNew" style="float:left; width:51px">Enable</label>
251 <input id="bubbleNew" type="checkbox" ${this.user.bubbleNew ? 'checked' : ''}></p>
252 </div>
253 <section style="margin-left:100px; margin-top:23px">
254 When this option is enabled, new HITs will always be placed at the top of the results table.
255 </section>
256 </div><div>
257 <div style="float:left; margin-left:15px">
258 <span style="position:relative; left:-8px"><b>Color Type</b></span>
259 <p><label for="ctSim" style="float:left; width:51px">Simple</label>
260 <input id="ctSim" type="radio" name="colorType" value="sim"
261 ${this.user.colorType === 'sim' ? 'checked' : ''}/></p>
262 <p><label for="ctAdj" style="float:left; width:51px">Adjusted</label>
263 <input id="ctAdj" type="radio" name="colorType" value="adj"
264 ${this.user.colorType === 'adj' ? 'checked' : ''}/></p>
265 </div>
266 <section style="margin-left:100px">
267 <span style="position:relative; left:10px"><i>simple</i></span><br>HIT Scraper will use a simple weighted average to
268 determine the overall TO rating and colorize results using that value. Use this setting to make coloring consistent between
269 HIT Scraper and <a style="color:black" href="${_ccs}" target="_blank">Color Coded Search</a>.
270 </section><section style="margin-left:100px">
271 <span style="position:relative; left:10px;"><i>adjusted</i></span><br>HIT Scraper will calculate a Bayesian adjusted average
272 based on confidence of the TO rating to colorize results. Confidence is proportional to the number of reviews.
273 </section>
274 </div><div>
275 <div style="float:left; margin-left:15px">
276 <span style="position:relative; left:-8px"><b>Sort Type</b></span>
277 <p><label for="stSim" style="float:left; width:51px">Simple</label>
278 <input id="stSim" type="radio" name="sortType" value="sim"
279 ${this.user.sortType === 'sim' ? 'checked' : ''}/></p>
280 <p><label for="stAdj" style="float:left; width:51px">Adjusted</label>
281 <input id="stAdj" type="radio" name="sortType" value="adj"
282 ${this.user.sortType === 'adj' ? 'checked' : ''}/></p>
283 </div>
284 <section style="margin-left:100px">
285 <span style="position:relative; left:10px"><i>simple</i></span><br>
286 HIT Scraper will sort results based simply on value regardless of the number of reviews.
287 </section><section style="margin-left:100px">
288 <span style="position:relative; left:10px;"><i>adjusted</i></span><br>HIT Scraper will use a Bayesian adjusted rating
289 based on reliability (i.e. confidence) of the data. It factors in the number of reviews such that, for example,
290 a requester with 100 reviews rated at 4.6 will rightfully be ranked higher than a requester with 3 reviews rated at 5.
291 This gives a more accurate representation of the data.
292 </section>
293 </div><div>
294 <div style="float:left; margin-left:15px">
295 <span style="position:relative; left:-8px"><b>Alert Volume</b></span>
296 <p><label style="float:left;width:45px">Ding</label>
297 <input name="ding" type="range" value=${this.user.volume.ding} max="1" step="0.02" min="0" />
298 <span style="padding-left:10px">${Math.floor(this.user.volume.ding * 100)}%</span></p>
299 <p><label style="float:left;width:45px">Squee</label>
300 <input name="squee" type="range" value=${this.user.volume.squee} max="1" step="0.02" min="0" />
301 <span style="padding-left:10px">${Math.floor(this.user.volume.squee * 100)}%</span></p>
302 </div>
303 </div><div>
304 <div style="float:left; margin-left:15px">
305 <span style="position:relative; left:-8px"><b>TO Weighting</b></span>
306 <p><label for="comm" style="float:left; width:45px">comm</label>
307 <input id="comm" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.comm} style="width:40px"/></p>
308 <p><label for="pay" style="float:left; width:45px">pay</label>
309 <input id="pay" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.pay} style="width:40px"/></p>
310 <p><label for="fair" style="float:left; width:45px">fair</label>
311 <input id="fair" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.fair} style="width:40px"/></p>
312 <p><label for="fast" style="float:left; width:45px">fast</label>
313 <input id="fast" type="number" name="TOW" min="1" max="10" step="0.5" value=${this.user.toWeights.fast} style="width:40px"/></p>
314 </div>
315 <section style="margin-left:110px; padding:10px">
316 Specify weights for TO attributes to place greater importance on certain attributes over others.
317 <p>The default values, [1, 3, 3, 1], ensure consistency between HIT Scraper and
318 <a style="color:black" href="${_ccs}" target="_blank">Color Coded Search</a>;
319 recommended values for adjusted coloring are [1, 6, 3.5, 1].</p>
320 </section>
321 </div>`,//}}}
322 _appearance =//{{{
323 `<div>
324 <div style="float:left; margin-left:15px">
325 <span style="position:relative;left:-8px"><b>Display Checkboxes</b></span>
326 <p><label for="checkshow" style="float:left;width:51px">Show</label>
327 <input id="checkshow" type="radio" name="checkbox" value="true"
328 ${this.user.showCheckboxes ? 'checked' : ''} /></p>
329 <p><label for="checkhide" style="float:left;width:51px">Hide</label>
330 <input id="checkhide" type="radio" name="checkbox" value="false"
331 ${this.user.showCheckboxes ? '' : 'checked'} /></p>
332 </div>
333 <section style="margin-left:133px">
334 <span style="position:relative;left:10px"><i>show</i></span><br>
335 Shows all checkboxes and radio inputs on the control panel for sake of clarity.
336 </section><section style="margin-left:133px">
337 <span style="position:relative;left:10px"><i>hide</i></span><br>
338 Hides checkboxes and radio inputs for a cleaner, neater appearance. Their visibility is not required for proper
339 operation; all options can still be toggled while hidden.
340 </section>
341 </div><div>
342 <div style="float:left;margin-left:15px">
343 <span style="position:relative;left:-8px"><b>Themes</b></span>
344 <p><select>
345 <option value="classic" ${this.user.themes.name === 'classic' ? 'selected' : ''}>Classic</option>
346 <option value="deluge" ${this.user.themes.name === 'deluge' ? 'selected' : ''}>Deluge</option>
347 <option value="solDark" ${this.user.themes.name === 'solDark' ? 'selected' : ''}>Solarium:Dark</option>
348 <option value="solLight" ${this.user.themes.name === 'solLight' ? 'selected' : ''}>Solarium:Light</option>
349 <option value="whisper" ${this.user.themes.name === 'whisper' ? 'selected' : ''}>Whisper</option>` +
350 //<option value="random" ${this.user.themes.name === 'random' ? 'selected' : ''}>I'm Feelin'
351 // Lucky!</option>
352 `</select> <button id="thedit" style="cursor:pointer">Edit Current Theme</button></p>
353 </div>
354 </div><div>
355 <div style="float:left;margin-left:15px">
356 <span style="position:relative;left:-8px"><b>HIT Coloring</b></span>
357 <p><label for="link" style="float:left;width:51px">Link</label>
358 <input id="link" type="radio" name="hitColor" value="link"
359 ${this.user.hitColor === 'link' ? 'checked' : ''} /></p>
360 <p><label for="cell" style="float:left;width:51px">Cell</label>
361 <input id="cell" type="radio" name="hitColor" value="cell"
362 ${this.user.hitColor === 'cell' ? 'checked' : ''} /></p>
363 </div>
364 <section style="margin-left:100px;padding-top:10px">
365 <span style="position:relative;left:10px"><i>link</i></span><br>
366 Apply coloring based on Turkopticon reviews to all applicable links in the results table.
367 </section><section style="margin-left:100px">
368 <span style="position:relative;left:10px"><i>cell</i></span><br>
369 Apply coloring based on Turkopticon reviews to the background of all applicable cells in the results table.
370 </section>
371 <p style="clear:both"><b>Note:</b> The Classic theme is exempt from these settings and will always colorize cells.</p>
372 </div><div>
373 <div style="float:left;margin-left:15px">
374 <span style="position:relative;left:-8px"><b>Font Size</b></span>
375 <p><input name="fontSize" type="number" min="5" value="${this.user.fontSize}" style="width:45px"></p>
376 <span style="position:relative;left:-8px"><b>New HIT Offset</b></span>
377 <p><input name="shineOffset" type="number" value="${this.user.shineOffset}" style="width:45px"></p>
378 </div>
379 <section style="margin-left:100px;margin-top:15px">
380 Change the font size (measured in px) for text in the results table. Default is 11px.
381 </section><section style="margin-left:100px;margin-top:40px;">
382 Controls the font size of new HITs relative to the rest of the results. Default is 1px. <br />
383 <i>Example:</i> With a font size of 11px and an offset of 1px, new HITs will be displayed at 12px.
384 </section>
385 </div>`,//}}}
386 _blocks = //{{{
387 `<div>
388 <div style="float:left; margin-left:15px">
389 <span style="position:relative; left:-8px"><b>Advanced Matching</b></span>
390 <p><label for="wildblocks" style="float:left; width:95px">Allow Wildcards</label>
391 <input id="wildblocks" type="checkbox" ${this.user.wildblocks ? 'checked' : ''}/></p>
392 </div>
393 <section style="margin-left:150px">
394 Allows for the use of asterisks <code>(*)</code> as wildcards in the blocklist for simple glob matching. Any blocklist entry
395 without an asterisk is treated the same as the default behavior--the entry must exactly match a HIT title or requester to
396 trigger a block.
397 <p><em>Wildcards have the potential to block more HITs than intended if using a pattern that's too generic.</em></p>
398 <p>Matching is not case sensitive regardless of the wildcard setting. Entries without an opening asterisk are
399 expected to match the beginning of a line, likewise, entries without a closing asterisk are expected to match
400 the end of a line. Example usage below.</p>
401 <table class="ble" style="left:-100px;position:relative;width:110%;">
402 <tr>
403 <th class="blec ble"></th>
404 <th class="blec ble">Matches</th>
405 <th class="blec ble" style="width:86px">Does not match</th>
406 <th class="blec ble">Notes</th>
407 </tr><tr>
408 <td rowspan="2" class="blec ble"><code>foo*baz</code></td>
409 <td class="blec ble">foo bar bat baz</td>
410 <td class="blec ble">bar foo bat baz</td>
411 <td rowspan="2" class="blec ble">no leading or closing asterisks; <code>foo</code> must be at the start of a line,
412 and <code>baz</code> must be at the end of a line for a positive match</td>
413 </tr><tr><td class="blec ble">foobarbatbaz</td><td class="blec ble">foo bar bat</td>
414 </tr><tr>
415 <td class="blec ble"><code>*foo</code></td>
416 <td class="ble blec">bar baz foo</td>
417 <td class="blec ble">foo baz</td>
418 <td class="ble blec">matches and blocks any line ending in <code>foo</code></td>
419 </tr><tr>
420 <td class="blec ble"><code>foo*</code></td>
421 <td class="ble blec">foo bat bar</td>
422 <td class="ble blec">bat foo baz</td>
423 <td class="ble blec">matches and blocks any line beginning with <code>foo</code></td>
424 </tr><tr>
425 <td class="ble blec" rowspan="4"><code>*bar*</code></td>
426 <td class="ble blec">foo bar bat baz</td>
427 <td class="ble blec" rowspan="4">foo bat baz</td>
428 <td class="ble blec" rowspan="4">matches and blocks any line containing <code>bar</code></td>
429 </tr><tr><td class="ble blec">bar bat baz</td>
430 </tr><tr><td class="ble blec">foo bar</td>
431 </tr><tr><td class="ble blec">foobatbarbaz</td>
432 </tr><tr>
433 <td class="ble blec"><code>** foo</code></td>
434 <td class="ble blec">** foo</td>
435 <td class="ble blec">** foo bar baz</td>
436 <td class="ble blec">Multiple consecutive asterisks will be treated as a string rather than a wildcard. This makes it
437 compatible with HITs using multiple asterisks in their titles, <i>e.g.</i>, <code>*** contains peanuts ***</code>.</td>
438 </tr><tr>
439 <td class="ble blec"><code>** *bar* ***</td>
440 <td class="ble blec">** foo bar baz bat ***</td>
441 <td class="ble blec">foo bar baz</td>
442 <td class="ble blec">Consecutive asterisks used in conjunction with single asterisks.</td>
443 </tr><tr>
444 <td class="ble blec"><code>*</code></td>
445 <td class="ble blec"><i>nothing</i></td>
446 <td class="ble blec"><i>all</i></td>
447 <td class="ble blec">A single asterisk would usually match anything and everything,
448 but here, it matches nothing. This prevents accidentally blocking everything from the results table.</td>
449 </tr>
450 </table>
451 </section>
452 </div>`,//}}}
453 _notify = //{{{
454 `<div>
455 <div style="float:left; margin-left:15px">
456 <span style="position:relative; left:-8px"><b>Additional Notifications</b></span><br>
457 <p><label for="notifyBlink" style="float:left; width:51px">Blink</label>
458 <input id="notifyBlink" type="checkbox" name="notify" ${this.user.notifyBlink ? 'checked' : ''}/></p>
459 <p><label for="notifyTaskbar" style="float:left; width:51px">Taskbar</label>
460 <input id="notifyTaskbar" type="checkbox" name="notify" ${this.user.notifyTaskbar ? 'checked' : ''}/></p>
461 </div>
462 <section style="margin-left:160px">
463 <span style="position:relative; left:10px"><i>blink</i></span><br>
464 Blink the tab when there are new HITs.
465 </section>
466 <section style="margin-left:160px">
467 <span style="position:relative; left:10px"><i>taskbar</i></span><br>
468 Create an HTML5 browser notification when there are new HITs, which appears over the taskbar for 10 seconds.
469 </section>
470 <p style="clear:both"><b>Note:</b> These notification options will only apply when the page does not have active focus.</p>
471 </div>`,//}}}
472 _utils =//{{{
473 `<div>
474 <div style="float:left; margin-left:15px">
475 <span style="position relative; left:-8px"><b>Export/Import</b></span>
476 <p><button id="sexport">Export</button></p>
477 <p><button id="simport">Import</button></p>
478 <input type="file" id="fsimport" style="display:none">
479 </div>
480 <section style="margin-left:130px; margin-top:15px">
481 <span style="position:relative; left:10px"><i>Export</i></span><br>
482 Export your current settings, block list, and include list as a local file.
483 </section>
484 <section style="margin-left:130px">
485 <span style="position:relative; left:10px"><i>Import</i></span></br>
486 Import your settings, block list, and include list from a local file.
487 </section>
488 <div style="margin-top:10px" id="eisStatus"></div>
489 </div>`,//}}}
490 _main = //{{{
491 `<div style="top:0;left:0;margin:0;text-align:right;padding:0px;border:none;width:100%">
492 <label id="settingsClose" class="close" title="Close"> ✘ </label>
493 </div><div id="settingsSidebar">
494 <span class="settingsSelected">General</span>
495 <span>Appearance</span>
496 <span>Blocklist</span>
497 <span>Notifications</span>
498 <span>Utilities</span>
499 </div><div id="panelContainer" style="margin-left:10px;border:none;overflow:auto;width:auto;height:92%">
500 <div id="General" class="settingsPanel">${_general}</div>
501 <div id="Appearance" class="settingsPanel">${_appearance}</div>
502 <div id="Blocklist" class="settingsPanel">${_blocks}</div>
503 <div id="Notifications" class="settingsPanel">${_notify}</div>
504 <div id="Utilities" class="settingsPanel">${_utils}</div>
505 </div>`;//}}}
506
507 this.main = document.body.appendChild(document.createElement('DIV'));
508 this.main.id = 'settingsMain';
509 this.main.innerHTML = _main;
510 return this;
511 },//}}} Settings::draw
512 init : function() {//{{{
513 var get = (q, all) => this.main['querySelector' + (all ? 'All' : '')](q),
514 sidebarFn = function(e) {
515 if (e.target.classList.contains('settingsSelected')) return;
516 get('#' + get('.settingsSelected').textContent).style.display = 'none';
517 get('.settingsSelected').classList.toggle('settingsSelected');
518 e.target.classList.toggle('settingsSelected');
519 get('#' + e.target.textContent).style.display = 'block';
520 }.bind(this),
521 sliderFn = function(e) {
522 e.target.nextElementSibling.textContent = Math.floor(e.target.value * 100) + '%';
523 },
524 optChangeFn = function(e) {//{{{
525 var tag = e.target.tagName, type = e.target.type, id = e.target.id,
526 isChecked = e.target.checked, name = e.target.name, value = e.target.value;
527
528 switch (tag) {
529 case 'SELECT':
530 //get('#thedit').textContent = value === 'random' ? 'Re-roll!' : 'Edit Current Theme';
531 this.user.themes.name = value;
532 Themes.apply(value, this.user.hitColor);
533 break;
534 case 'INPUT':
535 switch (type) {
536 case 'radio':
537 if (name === 'checkbox') {
538 this.user.showCheckboxes = (value === 'true');
539 Array.from(document.querySelectorAll('#controlpanel input[type=checkbox],#controlpanel input[type=radio]'))
540 .forEach(v => v.classList.toggle('hidden'));
541 }
542 else this.user[name] = value;
543 if (name === 'hitColor') Themes.apply(this.user.themes.name, value);
544 break;
545 case 'checkbox':
546 this.user[id] = isChecked;
547 if (name === 'export')
548 Array.from(document.querySelectorAll(`button.${value}`))
549 .forEach(v => v.style.display = isChecked ? '' : 'none');
550 if (id === 'notifyTaskbar' && isChecked && Notification.permission === 'default')
551 Notification.requestPermission();
552 break;
553 case 'number':
554 if (name === 'fontSize')
555 document.head.querySelector('#lazyfont').sheet.cssRules[0].style.fontSize = value + 'px';
556 else if (name === 'shineOffset')
557 document.head.querySelector('#lazyfont').sheet.cssRules[1].style.fontSize = +this.user.fontSize + (+value) + 'px';
558 if (name === 'TOW') this.user.toWeights[id] = value;
559 else this.user[name] = value;
560 break;
561 case 'range':
562 this.user.volume[name] = value;
563 let audio = document.querySelector(`#${name}`);
564 audio.volume = value;
565 audio.play();
566 break;
567 }
568 break;
569 }
570 Settings.save();
571 }.bind(this);//}}}
572
573 get('#settingsClose').onclick = this.die.bind(this);
574 get('#General').style.display = 'block';
575 Array.from(get('#settingsSidebar span', true)).forEach(v => v.onclick = sidebarFn);
576 Array.from(get('input:not([type=file]),select', true)).forEach(v => v.onchange = optChangeFn);
577 Array.from(get('input[type=range]', true)).forEach(v => v.oninput = sliderFn);
578 get('#thedit').onclick = () => {
579 this.die.call(this);
580 new Editor('theme');
581 };
582 get('#sexport').onclick = FileHandler.exports;
583 get('#simport').onclick = () => {
584 get('#fsimport').value = '';
585 get('#eisStatus').innerHTML = '';
586 get('#fsimport').click();
587 };
588 get('#fsimport').onchange = FileHandler.imports;
589 },//}}} Settings::init
590 die : function() {
591 Interface.toggleOverflow('off');
592 this.main.remove();
593 }
594 },//}}} Settings
595
596 Themes = {//{{{
597 default : defaults.themes,
598 generateCSS : function(theme, mode) {//{{{
599 var ref = theme === 'random' ? this.randomize() : Settings.user.themes.colors[theme],
600 _ms = mode === 'cell' || theme === 'classic',
601 cellFix = {
602 row : k => `.${k} ` + (_ms ? '{background:' : 'a {color:') + ref[k] + '}',
603 text : k => `.${k} {color:` + (_ms ? this.tune(ref.bodytable, ref[k]) : ref.bodytable) + '}',
604 export: k => `.${k} button {color:` + (_ms ? this.tune(ref.export, ref[k]) : ref.export) + '}',
605 vlink : k => `.${k} a:not(.static):visited {color:` + (_ms
606 ? this.tune(ref.vlink, ref[k])
607 : ref.vlink) + '}'
608 },
609 css = `body {color:${ref.defaultText}; background-color:${ref.background}}
610 /*#status {color:${ref.secondText}}*/
611 #sortdirs {color:${ref.inputText}}
612 #curtain {background:${ref.background}; opacity:0.5}
613 .controlpanel i:after {color:${ref.accent}}
614 #controlpanel {background:${ref.cpBackground}}
615 #controlpanel input${theme === 'classic' ? '' : ', #controlpanel select'}
616 {color:${ref.inputText}; border:1px solid; background:${theme === 'classic' ? '#fff' : ref.cpBackground}}
617 #controlpanel label {color:${ref.defaultText}; background:${ref.cpBackground}}
618 #controlpanel label:hover {background:${ref.hover}}
619 #controlpanel label.checked {color:${ref.secondText}; background:${ref.highlight}}
620 /*#resultsTable tbody a:not(.static):visited {color:${ref.vlink}}*/
621 /*#resultsTable button {color:${ref.export}}*/
622 thead, caption, a {color:${ref.defaultText}}
623 tbody a {color:${ref.link}}
624 .nohitDB {color:#000; background:${ref.nohitDB}}
625 .hitDB {color:#000; background:${ref.hitDB}}
626 .reqmaster {color:#000; background:${ref.reqmaster}}
627 .nomaster {color:#000; background:${ref.nomaster}}
628 .tooweak {background:${ref.unqualified}}
629 ${cellFix.row('toNone')} ${cellFix.text('toNone')} ${cellFix.export('toNone')} ${cellFix.vlink('toNone')}
630 ${cellFix.row('toHigh')} ${cellFix.text('toHigh')} ${cellFix.export('toHigh')} ${cellFix.vlink('toHigh')}
631 ${cellFix.row('toGood')} ${cellFix.text('toGood')} ${cellFix.export('toGood')} ${cellFix.vlink('toGood')}
632 ${cellFix.row('toAverage')} ${cellFix.text('toAverage')} ${cellFix.export('toAverage')} ${cellFix.vlink('toAverage')}
633 ${cellFix.row('toLow')} ${cellFix.text('toLow')} ${cellFix.export('toLow')} ${cellFix.vlink('toLow')}
634 ${cellFix.row('toPoor')} ${cellFix.text('toPoor')} ${cellFix.export('toPoor')} ${cellFix.vlink('toPoor')}`;
635 if (theme !== 'classic') css += `\n.controlpanel button {color:${ref.accent}; background:transparent;}`;
636 return css;
637 },//}}} Themes::generateCSS
638 tune : function(fg, bg) {//{{{
639 var cbg = this.getBrightness(bg),
640 lighten = c => {
641 c.s = Math.max(0, c.s - 5);
642 c.v = Math.min(100, c.v + 5);
643 return c;
644 },
645 darken = c => {
646 c.s = Math.min(100, c.s + 5);
647 c.v = Math.max(0, c.v - 5);
648 return c;
649 },
650 tune = (function() { if (cbg >= 128) return darken; else return lighten; })(),
651 hex2hsv = function(c) {//{{{
652 var r = parseInt(c.slice(1, 3), 16), g = parseInt(c.slice(3, 5), 16), b = parseInt(c.slice(5, 7), 16),
653 min = Math.min(r, g, b), max = Math.max(r, g, b), delta = max - min, _hue;
654 switch (max) {
655 case r:
656 _hue = Math.round(60 * (g - b) / delta);
657 break;
658 case g:
659 _hue = Math.round(120 + 60 * (b - r) / delta);
660 break;
661 case b:
662 _hue = Math.round(240 + 60 * (r - g) / delta);
663 break;
664 }
665 return {
666 h: _hue < 0 ? _hue + 360 : _hue,
667 s: max === 0 ? 0 : Math.round(100 * delta / max),
668 v: Math.round(max * 100 / 255)
669 };
670 }, //}}}
671 hsv2hex = function(c) {//{{{
672 var r, g, b, pad = s => ('00' + s.toString(16)).slice(-2);
673 if (c.s === 0) r = g = b = Math.round(c.v * 2.55).toString(16);
674 else {
675 c = { h: c.h / 60, s: c.s / 100, v: c.v / 100 }; // convert to prime to calc chroma
676 var _t1 = Math.round((c.v * (1 - c.s)) * 255),
677 _t2 = Math.round((c.v * (1 - c.s * (c.h - Math.floor(c.h)))) * 255),
678 _t3 = Math.round((c.v * (1 - c.s * (1 - (c.h - Math.floor(c.h))))) * 255);
679 switch (Math.floor(c.h)) {
680 case 1:
681 r = _t2;
682 g = Math.round(c.v * 255);
683 b = _t1;
684 break;
685 case 2:
686 r = _t1;
687 g = Math.round(c.v * 255);
688 b = _t3;
689 break;
690 case 3:
691 r = _t1;
692 g = _t2;
693 b = Math.round(c.v * 255);
694 break;
695 case 4:
696 r = _t3;
697 g = _t1;
698 b = Math.round(c.v * 255);
699 break;
700 case 0:
701 r = Math.round(c.v * 255);
702 g = _t3;
703 b = _t1;
704 break;
705 default:
706 r = Math.round(c.v * 255);
707 g = _t1;
708 b = _t2;
709 break;
710 }
711 }
712 return '#' + pad(r) + pad(g) + pad(b);
713 };//}}}
714
715 while (Math.abs(this.getBrightness(fg) - cbg) < 90) fg = hsv2hex(tune(hex2hsv(fg)));
716 return fg;
717 },//}}}
718 getBrightness: function(hex) {//{{{
719 // TODO: put in Colors object
720 var r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16);
721 return ((r * 299) + (g * 587) + (b * 114)) / 1000;
722 },//}}} Themes::getBrightness
723 apply : function(theme, mode) {//{{{
724 var cssNew = URL.createObjectURL(new Blob([this.generateCSS(theme, mode)], { type: 'text/css' })),
725 rel = document.head.querySelector('link[rel=stylesheet]'), cssOld = rel.href;
726 rel.href = cssNew;
727 URL.revokeObjectURL(cssOld);
728 }//}}} Themes::apply
729 },//}}} Themes
730
731 Interface = {//{{{
732 user : Settings.user,
733 time : Date.now(),
734 focused : true,
735 blackhole : {},
736 isLoggedout : document.querySelector('#lnkWorkerSignin') ? true : false,
737 resetTitle : function() {//{{{
738 if (this.blackhole.blink) clearInterval(this.blackhole.blink);
739 document.title = DOC_TITLE;
740 },//}}}
741 toggleOverflow: function(state) {//{{{
742 document.body.querySelector('#curtain').style.display = state === 'on' ? 'block' : 'none';
743 document.body.style.overflow = state === 'on' ? 'hidden' : 'auto';
744 },//}}} Interface::curtains
745 draw : function() {//{{{
746 var user = this.user = Settings.user,
747 _cb = user.showCheckboxes ? '' : 'hidden',
748 _u0 = new Uint8Array(Array.prototype.map.call(window.atob(audio0), v => v.charCodeAt(0))),
749 _u1 = new Uint8Array(Array.prototype.map.call(window.atob(audio1), v => v.charCodeAt(0))),
750 ding = URL.createObjectURL(new Blob([_u0], { type: 'audio/ogg' })),
751 squee = URL.createObjectURL(new Blob([_u1], { type: 'audio/mp3' })),
752 titles = {//{{{
753 refresh : 'Enter search refresh delay in seconds.\nEnter 0 for no auto-refresh.\nDefault is 0 (no auto-refresh).',
754 pages : 'Enter number of pages to scrape. Default is 1.',
755 skips : 'Searches additional pages to get a more consistent number of results. Helpful if you\'re blocking a lot of items.',
756 resultsPerPage: 'Number of results to return per page (maximum is 100, default is 30)',
757 batch : 'Enter minimum HITs for batch search (must be searching by Most Available).',
758 pay : 'Enter the minimum desired pay per HIT (e.g. 0.10).',
759 qual : 'Only show HITs you\'re currently qualified for (must be logged in).',
760 monly : 'Only show HITs that require Masters qualifications.',
761 mhide : 'Remove masters hits from the results if selected, otherwise display both masters and non-masters HITS.\n' +
762 'The \'qualified\' setting supercedes this option.',
763 searchBy : 'Get search results by...\n Latest = HIT Creation Date (newest first),\n ' +
764 'Most Available = HITs Available (most first),\n Reward = Reward Amount (most first),\n Title = Title (A-Z)',
765 invert : 'Reverse the order of the Search By choice, so...\n Latest = HIT Creation Date (oldest first),\n ' +
766 'Most Available = HITs Available (least first),\n Reward = Reward Amount (least first),\n Title = Title (Z-A)',
767 shine : 'Enter time (in seconds) to keep new HITs highlighted.\nDefault is 300 (5 minutes).',
768 sound : 'Play a sound when new results are found.',
769 soundSelect : 'Select which sound will be played.',
770 minTOPay : 'After getting search results, hide any results below this average Turkopticon pay rating.\n' +
771 'Minimum is 1, maximum is 5, decimals up to 2 places, such as 3.25',
772 hideNoTO : 'After getting search results, hide any results that have no, or too few, Turkopticon pay ratings.',
773 disableTO : 'Disable attempts to download ratings data from Turkopticon for the results table.\n' +
774 'NOTE: TO is cached. That means if TO is availible from a previous scrape, it will use that value even if ' +
775 'TO is disabled. This option only prevents the retrieval of ratings from the Turkopticon servers,',
776 sortPay : 'After getting search results, re-sort the results based on their average Turkopticon pay ratings.',
777 sortAll : 'After getting search results, re-sort the results by their overall Turkopticon rating.',
778 sortAsc : 'Sort results in ascending (low to high) order.',
779 sortDsc : 'Sort results in descending (high to low) order.',
780 search : 'Enter keywords to search for; default is blank (no search terms).',
781 hideBlock : 'When enabled, hide HITs that match your blocklist.\n' +
782 'When disabled, HITs that match your blocklist will be displayed with a red border.',
783 onlyIncludes : 'Show only HITs that match your includelist.\nBe sure to edit your includelist first or no results will be displayed.',
784 shineInc : 'Outline HITs that match your includelist with a dashed green border.',
785 mainlink : 'Version: ' + ENV.VERSION + '\nRead the documentation for HIT Scraper With Export on its Greasyfork page.',
786 gbatch : 'Apply the \'Minimum batch size\' filter to all search options.',
787 onlyViable : 'Filters out HITs with qualifications you do not have and \ncan neither request nor take a test to obtain.\n' +
788 'Does not work while logged out.'
789 },//}}}
790 css = [//{{{
791 'body {font-family:Verdana, Arial; font-size:14px}',
792 'p {margin:8px auto}',
793 '.cpdefault {display:inline-block; visibility:visible; overflow:hidden; padding:8px 5px 1px 5px; transition:all 0.3s;}',
794 '#controlpanel i:after, #status i:after {content:" | "}',
795 'input[type="checkbox"], input[type="radio"] {vertical-align:middle}',
796 'input[type="number"] {width:50px; text-align:center}',
797 'label {padding:2px}',
798 '.hiddenpanel {width:0px; height:0px; visibility:hidden}',
799 '.hidden {display:none}',
800 'button {border:1px solid}',
801 'textarea {font-family:inherit; font-size:11px; margin:auto; padding:2px}',
802 '.pop {position:fixed; top:15%; left:50%; margin:auto; transform:translateX(-50%); padding:5px;' + // for
803 // editors/exporters
804 'background:black; color:white; z-index:20; font-size:12px; box-shadow:0px 0px 6px 1px #fff}',
805 'dt {text-transform:uppercase; clear:both; margin:3px}',
806 '.icbutt {float:left;border:1px solid #fff;cursor:pointer} .icbutt > input {opacity:0;display:block;width:25px;height:25px;border:none}',
807 // settings
808 '#settingsMain {z-index:20; position:fixed; background:#fff; color:#000; box-shadow:-3px 3px 2px 2px #7B8C89; line-height:initial;' +
809 'top:50%; left:50%; width:85%; height:85%; margin-right:-50%; transform:translate(-50%, -50%)}',
810 '#settingsMain > div {margin:5px; padding:3px; position:relative; border:1px solid grey; line-height:initial}',
811 '.close {position:relative; font-weight:bold; font-size:1em; color:white; background:black; cursor:pointer}',
812 '#settingsSidebar {width:100px; min-width:90px; height:92%; float:left}',
813 '#settingsSidebar > span {display:block; margin-bottom:5px; width:100px; font-size:1em; cursor:pointer}',
814 '.settingsPanel {position:absolute; top:0;left:0; display:none; width:100%; height:100%; font-size:11px}',
815 '.settingsPanel > div {margin:15px 5px; position:relative; background:#CCFFFA; overflow:auto; padding:6px 10px}',
816 '.settingsSelected {background:aquamarine}',
817 '.ble {border:1px solid black; border-collapse:collapse;} .blec {padding:5px; text-align:left;}',
818
819 '.toLink {position:relative;}',
820 '.toLink:before {content:""; display:none; z-index:5; position:absolute; top:0; left:-6px; width:0; height:0;' +
821 'border-top:6px solid transparent; border-bottom:6px solid transparent; border-left:6px solid black}',
822 '.toLink:hover:before {display:block;}',
823 '.tooltip {position:absolute;top:0;right:calc(100% + 6px);text-align:left;transform:translateY(-20%);padding:5px;font-weight:normal;' +
824 'font-size:11px; line-height:1; display:none; background:black; color:white; box-shadow:0px 0px 6px 1px #fff}',
825 'meter {width:100%; position:relative; height:15px;}',
826 'meter:before, .ffmb {display:block; font-size:10px; font-weight:bold; color:black; content:attr(data-attr); position:absolute; top:1px}',
827 'meter:after, .ffma {display:block; font-size:10px; font-weight:bold; color:black; content:attr(value); position:absolute; top:1px; right:0}',
828 '#resultsTable button {height:14px; font-size:8px; border:1px solid; padding:0; background:transparent}',
829 '#resultsTable tbody td > div {display:table-cell}',
830 '#resultsTable tbody td > div:first-child {padding-right:2px; vertical-align:middle; white-space:nowrap}',
831 'button.disabled {position:relative}',
832 'button.disabled:before {content:""; display:none; z-index:5; position:absolute; top:-7px; left:50%; width:0; height:0;' +
833 'border-left:6px solid transparent; border-right:6px solid transparent; border-top:6px solid black; transform:translateX(-50%)}',
834 'button.disabled:after {content:"Exports are disabled while logged out."; display:none; z-index:5; position:absolute;' +
835 'top:-7px; left:50%; color:white; background:black; width:230px; padding:2px; transform:translate(-50%,-100%);' +
836 'box-shadow:0px 0px 6px 1px #fff; font-size:12px}',
837 'button.disabled:focus:before {display:block} button.disabled:focus:after {display:block}',
838 '.spinner {display: inline-block; animation: kfspin 0.7s infinite linear; font-weight:bold;}',
839 '@keyframes kfspin { 0% { transform: rotate(0deg) } 100% { transform: rotate(359deg) } }',
840 '.spinner:before{content:"*"}',
841 '.exhwtf {width:70px; background:black; color:white; vertical-align:top; border-radius:5px}',
842 '.ignored td {border:2px solid #00E5FF}',
843 '.includelisted td {border:3px dashed #008800}',
844 '.blocklisted td {border:3px solid #cc0000}'
845 ],//}}}
846 fCss =
847 `#resultsTable tbody {font-size:${user.fontSize}px;}` +
848 `.shine td {border:1px dotted #fff; font-size:${(+user.fontSize) + (+user.shineOffset)}px; font-weight:bold}`,
849 //{{{ body
850 body = `
851 <audio id="ding"> <source src=${ding}> </audio>
852 <audio id="squee"> <source src=${squee}> </audio>
853 <div id="curtain" style="position:fixed;width:100%;height:100%;display:none;z-index:10"></div>
854 <div id="controlpanel" class="controlpanel cpdefault">
855 <p>
856 Auto-refresh delay: <input id="refresh" type="number" title="${titles.refresh}" min="0" value="${user.refresh}" /><i></i>
857 Pages to scrape: <input id="pages" type="number" title="${titles.pages}" min="1" max="100" value="${user.pages}" /><i></i>
858 <label class="${user.skips ? 'checked' : ''}" title="${titles.skips}" for="skips">Correct for skips</label>
859 <input id="skips" class="${_cb}" type="checkbox" title="${titles.skips}" ${user.skips ? 'checked' : ''} /><i></i>
860 Results per page: <input id="resultsPerPage" type="number" title="${titles.resultsPerPage}"
861 min="1" max="100" value="${user.resultsPerPage || 10}" />
862 </p></p>
863 Min reward: <input id="pay" type="number" title="${titles.pay}" min="0" step="0.05" value="${user.pay}" /><i></i>
864 <label class="${user.qual ? 'checked' : ''}" title="${titles.qual}" for="qual">Qualified</label>
865 <input id="qual" class="${_cb}" type="checkbox" title="${titles.qual}" ${user.qual ? 'checked' : ''} /><i></i>
866 <label class="${user.monly ? 'checked' : ''}" title="${titles.monly}" for="monly">Masters Only</label>
867 <input id="monly" class="${_cb}" type="checkbox" title="${titles.monly}" ${user.monly ? 'checked' : ''} /><i></i>
868 <label class="${user.mhide ? 'checked' : ''}" title="${titles.mhide}" for="mhide">Hide Masters</label>
869 <input id="mhide" class="${_cb}" type="checkbox" title="${titles.mhide}" ${user.mhide ? 'checked' : ''} /><i></i>
870 <label class="${user.onlyViable ? 'checked' : ''}" title="${titles.onlyViable}" for="onlyViable">Hide Infeasible</label>
871 <input id="onlyViable" class="${_cb}" type="checkbox" title="${titles.onlyViable}" ${user.onlyViable
872 ? 'checked'
873 : ''} /><i></i>
874 Min batch size: <input id="batch" type="number" title="${titles.batch}" min="1" value="${user.batch}" /> -
875 <label class="${user.gbatch ? 'checked' : ''}" title="${titles.gbatch}" for="gbatch">Global</label>
876 <input id="gbatch" class="${_cb}" type="checkbox" title="${titles.gbatch}" ${user.gbatch ? 'checked' : ''} />
877 </p><p>
878 New HIT highlighting: <input id="shine" type="number" title="${titles.shine}" min="0" max="3600" value="${user.shine}" /><i></i>
879 <label class="${user.notifySound[0] ? 'checked' : ''}" title="${titles.sound}" for="sound">Sound on new HIT</label>
880 <input id="sound" class="${_cb}" type="checkbox" title="${titles.sound}" ${user.notifySound[0]
881 ? 'checked'
882 : ''} />
883 <select id="soundSelect" title="${titles.soundSelect}" style="display:${user.notifySound[0]
884 ? 'inline'
885 : 'none'}">
886 <option value="ding" ${user.notifySound[1] === 'ding' ? 'selected' : ''}>Ding</option>
887 <option value="squee" ${user.notifySound[1] === 'squee' ? 'selected' : ''}>Squee</option>
888 </select><i></i>
889 <label class="${user.disableTO ? 'checked' : ''}" title="${titles.disableTO}" for="disableTO">Disable TO</label>
890 <input id="disableTO" class="${_cb}" type="checkbox" title="${titles.disableTO}" ${user.disableTO
891 ? 'checked'
892 : ''} /><i></i>
893 Search by: <select id="searchBy" title="${titles.searchBy}">
894 <option value="late" ${user.searchBy === 0 ? 'selected' : ''}>Latest</option>
895 <option value="most" ${user.searchBy === 1 ? 'selected' : ''}>Most Available</option>
896 <option value="amount" ${user.searchBy === 2 ? 'selected' : ''}>Reward</option>
897 <option value="alpha" ${user.searchBy === 3 ? 'selected' : ''}>Title</option>
898 </select> -
899 <label class="${user.invert ? 'checked' : ''}" title="${titles.invert}" for="invert">Invert</label>
900 <input id="invert" class="${_cb}" type="checkbox" title="${titles.invert}" ${user.invert ? 'checked' : ''} />
901 </p><p>
902 Min pay TO: <input id="minTOPay" type="number" title="${titles.minTOPay}" min="1" max="5" step="0.25" value="${user.minTOPay}" /><i></i>
903 <label class="${user.hideNoTO ? 'checked' : ''}" title="${titles.hideNoTO}" for="hideNoTO">Hide no TO</label>
904 <input id="hideNoTO" class="${_cb}" type="checkbox" title="${titles.hideNoTO}" ${user.hideNoTO
905 ? 'checked'
906 : ''} /><i></i>
907 <label class="${user.sortPay ? 'checked' : ''}" title="${titles.sortPay}" for="sortPay">Sort by TO pay</label>
908 <input id="sortPay" class="${_cb}" type="checkbox" title="${titles.sortPay}" name="sort" ${user.sortPay
909 ? 'checked'
910 : ''} /><i></i>
911 <label class="${user.sortAll ? 'checked' : ''}" title="${titles.sortAll}" for="sortAll">Sort by overall TO</label>
912 <input id="sortAll" class="${_cb}" type="checkbox" title="${titles.sortAll}" name="sort" ${user.sortAll
913 ? 'checked'
914 : ''} />
915 <div id="sortdirs" style="font-size:15px;display:${user.sortPay || user.sortAll ? 'inline' : 'none'}">
916 <label class="${user.sortAsc ? 'checked' : ''}" for="sortAsc" title="${titles.sortAsc}"> ▲ </label>
917 <input id="sortAsc" class="${_cb}" type="radio" title="${titles.sortAsc}" name="sortDir" ${user.sortAsc
918 ? 'checked'
919 : ''} />
920 <label class="${user.sortDsc ? 'checked' : ''}" for="sortDsc" title="${titles.sortDsc}"> ▼ </label>
921 <input id="sortDsc" class="${_cb}" type="radio" title="${titles.sortDsc}" name="sortDir" ${user.sortDsc
922 ? 'checked'
923 : ''} />
924 </div>
925 </p><p>
926 Search Terms: <input id="search" title="${titles.search}" placeholder="Enter search terms here" value="${user.search}" /><i></i>
927 <label class="${user.hideBlock ? 'checked' : ''}" title="${titles.hideBlock}" for="hideBlock">Hide blocklisted</label>
928 <input id="hideBlock" class="${_cb}" type="checkbox" title="${titles.hideBlock}" ${user.hideBlock
929 ? 'checked'
930 : ''} /><i></i>
931 <label class="${user.onlyIncludes ? 'checked' : ''}" title="${titles.onlyIncludes}" for="onlyIncludes">Restrict to includelist</label>
932 <input id="onlyIncludes" class="${_cb}" type="checkbox" title="${titles.onlyIncludes}" ${user.onlyIncludes
933 ? 'checked'
934 : ''} /><i></i>
935 <label class="${user.shineInc ? 'checked' : ''}" title="${titles.shineInc}" for="shineInc">Highlight Includelisted</label>
936 <input id="shineInc" class="${_cb}" type="checkbox" title="${titles.shineInc}" ${user.shineInc
937 ? 'checked'
938 : ''} />
939 </p>
940 </div><div id="controlbuttons" class="controlpanel" style="margin-top:5px">
941 <button id="btnMain">Start</button> <button id="btnHide">Hide Panel</button> <button id="btnBlocks">Edit Blocklist</button>
942 <button id="btnIncs">Edit Includelist</button> <button id="btnIgnores">Toggle Ignored HITs</button>
943 <button id="btnSettings">Settings</button>
944 </div>
945 <div id="loggedout" style="font-size:11px;margin-left:10px;text-transform:uppercase"></div>
946 <div id="status" style="height:34px"><p>Stopped</p></div>
947 <div id="results">
948 <table id="resultsTable" style="width:100%">
949 <caption style="font-weight:800;line-height:1.25em;font-size:1.5em;">
950 <a class="mainlink" target="_blank" href="${URL_SELF}" title="${titles.mainlink}">HIT Scraper</a> Results
951 </caption>
952 <thead><tr style="font-weight:800;font-size:0.87em;text-align:center">
953 <td>Requester</td><td>Title</td><td style="width:70px">Reward & PandA</td><td style="width:35px"># Avail</td>
954 <td style="width:30px">TO Pay</td><td style="width:15px">M</td>
955 <td style="width:15px"></td><td style="width:15px"></td>
956 </tr></thead>
957 <tbody></tbody>
958 </table>
959 </div>`,//}}}
960 head = `<title>${DOC_TITLE}</title>` +
961 `<style type="text/css" id="lazyfont">${fCss}</style>` +
962 `<style type="text/css">${css.join('')}</style>` +
963 `<link rel="icon" type="image/png" href="${ico}" /><link rel="stylesheet" type="text/css" />`;
964
965 document.head.innerHTML = head;
966 document.body.innerHTML = body;
967 this.elkeys = Object.keys(titles);
968 return this;
969 },//}}} Interface::draw
970 init : function() {//{{{
971 this.panel = {};
972 this.buttons = {};
973 var get = (q, all) => document['querySelector' + (all ? 'All' : '')](q),
974 sortdirs = get('#sortdirs'),
975 moveSortdirs = function(node) {
976 if (!node.checked) {
977 sortdirs.style.display = 'none';
978 return;
979 }
980 sortdirs.style.display = 'inline';
981 sortdirs.remove();
982 node.parentNode.insertBefore(sortdirs, node.nextSibling);
983 },
984 kdFn = e => { if (e.keyCode === kb.ENTER) setTimeout(() => this.buttons.main.click(), 30); },
985 optChangeFn = function(e) {//{{{
986 var tag = e.target.tagName, type = e.target.type, id = e.target.id,
987 isChecked = e.target.checked, name = e.target.name, value = e.target.value;
988
989 switch (tag) {
990 case 'SELECT':
991 if (id === 'soundSelect')
992 this.user.notifySound[1] = e.target.value;
993 else
994 this.user[id] = e.target.selectedIndex;
995 break;
996 case 'INPUT':
997 switch (type) {
998 case 'number':
999 case 'text':
1000 this.user[id] = value;
1001 break;
1002 case 'radio':
1003 Array.from(get(`input[name=${name}]`, true))
1004 .forEach(v => {
1005 this.user[v.id] = v.checked;
1006 get(`label[for=${v.id}]`).classList.toggle('checked');
1007 });
1008 break;
1009 case 'checkbox':
1010 if (name === 'sort') {
1011 Array.from(get(`input[name=${name}]`, true)).forEach(v => {
1012 if (e.target !== v) v.checked = false;
1013 get(`label[for=${v.id}]`).className = v.checked ? 'checked' : '';
1014 this.user[v.id] = v.checked;
1015 });
1016 moveSortdirs(e.target);
1017 break;
1018 } else if (id === 'sound') {
1019 this.user.notifySound[0] = isChecked;
1020 e.target.nextElementSibling.style.display = isChecked ? 'inline' : 'none';
1021 }
1022 this.user[id] = isChecked;
1023 get(`label[for=${id}]`).classList.toggle('checked');
1024 break;
1025 }
1026 break;
1027 }
1028 Settings.save();
1029 }.bind(this);//}}}
1030
1031 'ding squee'.split(' ').forEach(v => get(`#${v}`).volume = this.user.volume[v]);
1032
1033 Themes.apply(this.user.themes.name);
1034 if (this.isLoggedout) get('#loggedout').textContent = 'you are currently logged out of mturk';
1035 // get references to control panel elements and set up click events
1036 this.Status = {
1037 node : get('#status').firstChild,
1038 push : function(t) { this.node.innerHTML = t; },
1039 append: function(t) { this.node.innerHTML += t; },
1040 cd : function() { this.node.innerHTML = this.node.innerHTML.replace(/\d+(?= seconds)/, m => +m - 1); }
1041 };
1042 for (var k of this.elkeys) {
1043 if (k === 'mainlink') continue;
1044 this.panel[k] = document.getElementById(k);
1045 this.panel[k].onchange = optChangeFn;
1046 if (k === 'pay' || k === 'search') this.panel[k].onkeydown = kdFn;
1047 if ((k === 'sortPay' || k === 'sortAll') && this.panel[k].checked) moveSortdirs(this.panel[k]);
1048 }
1049
1050 // get references to buttons
1051 Array.from(get('button', true)).forEach(v => this.buttons[v.id.slice(3).toLowerCase()] = v);
1052 // set up button click events
1053 this.buttons.main.onclick = function(e) {
1054 e.target.textContent = e.target.textContent === 'Start' ? 'Stop' : 'Start';
1055 Core.run();
1056 };
1057 this.buttons.hide.onclick = function(e) {
1058 get('#controlpanel').classList.toggle('hiddenpanel');
1059 e.target.textContent = e.target.textContent === 'Hide Panel' ? 'Show Panel' : 'Hide Panel';
1060 };
1061 this.buttons.blocks.onclick = () => {
1062 this.toggleOverflow('on');
1063 new Editor('ignore');
1064 };
1065 this.buttons.incs.onclick = () => {
1066 this.toggleOverflow('on');
1067 new Editor('include');
1068 };
1069 this.buttons.ignores.onclick = () => Array.from(get('.ignored:not(.blocklisted)', true)).forEach(v => v.classList.toggle('hidden'));
1070 this.buttons.settings.onclick = () => {
1071 this.toggleOverflow('on');
1072 Settings.draw().init();
1073 };
1074 get('#hideBlock').addEventListener('change', () => Array.from(get('.blocklisted', true)).forEach(v => v.classList.toggle('hidden')));
1075 document.body.onblur = () => this.focused = false;
1076 document.body.onfocus = () => {
1077 this.focused = true;
1078 this.resetTitle();
1079 };
1080 }//}}} Interface::init
1081 },//}}} Interface
1082
1083 Editor = function(type) {//{{{
1084 if (!type) return { setDefaultBlocks: setDefaultBlocks };
1085 Interface.toggleOverflow('on');
1086 this.node = document.body.appendChild(document.createElement('DIV'));
1087 this.node.classList.add('pop');
1088 this.die = () => {
1089 Interface.toggleOverflow('off');
1090 this.node.remove();
1091 };
1092 this.type = type;
1093 this.caller = arguments[1] || null;
1094
1095 function setDefaultBlocks() {
1096 return localStorage.setItem('scraper_ignore_list',
1097 'oscar smith^diamond tip research llc^jonathan weber^jerry torres^' +
1098 'crowdsource^we-pay-you-fast^turk experiment^jon brelig^p9r^scoutit');
1099 }
1100
1101 switch (type) {
1102 case 'include':
1103 case 'ignore':
1104 if (type === 'ignore' && !localStorage.getItem('scraper_ignore_list')) setDefaultBlocks();
1105 var titleText = type === 'ignore'
1106 ? '<b>BLOCKLIST</b> - Edit the blocklist with what you want to ignore/hide. Separate requester names and HIT titles with the ' +
1107 '<code>^</code> character. After clicking "Save", you\'ll need to scrape again to apply the changes.'
1108 : '<b>INCLUDELIST</b> - Focus the results on your favorite requesters. Separate requester names and HIT titles with the ' +
1109 '<code>^</code> character. When the "Restrict to includelist" option is selected, ' +
1110 'HIT Scraper only shows results matching the includelist.';
1111 this.node.innerHTML = '<div style="width:500px">' + titleText + '</div>' +
1112 '<textarea style="display:block;height:200px;width:500px;font:12px monospace" placeholder="nothing here yet">' +
1113 (localStorage.getItem(`scraper_${type}_list`) || '') + '</textarea>' +
1114 '<button id="edSave" style="margin:5px auto;width:50%;color:white;background:black">Save</button>' +
1115 '<button id="edCancel" style="margin:5px auto;width:50%;color:white;background:black">Cancel</button>';
1116 this.node.querySelector('#edSave').onclick = () => {
1117 localStorage.setItem(`scraper_${type}_list`, this.node.querySelector('textarea').value.trim());
1118 this.die();
1119 };
1120 break;
1121 case 'theme':
1122 var dlbody = [], _th = Settings.user.themes, split = obj => {
1123 var a = [];
1124 for (var k in obj) if (obj.hasOwnProperty(k)) a.push({ k: k, v: obj[k] });
1125 return a.sort((a, b) => a.k < b.k ? -1 : 1);
1126 }, _colors = split(_th.colors[_th.name]),
1127 define = k => '<div style="margin-left:37px">' + _dd[k] + '</div>',
1128 _dd = {//{{{
1129 highlight : 'Distinguishes between active and inactive states in the control panel',
1130 background : 'Background color',
1131 accent : 'Color of spacer text (and control panel buttons on themes other than \'classic\')',
1132 bodytable : 'Default color of text elements in the results table (this is ignored if HIT coloring is set to \'cell\')',
1133 cpBackground: 'Background color of the control panel',
1134 toHigh : 'Color for results with high TO',
1135 toGood : 'Color for results with good TO',
1136 toAverage : 'Color for results with average TO',
1137 toLow : 'Color for results with low TO',
1138 toPoor : 'Color for results with poor TO',
1139 toNone : 'Color for results with no TO',
1140 hitDB : 'Designates that a match was found in your HITdb',
1141 nohitDB : 'Designates that a match was not found in your HITdb',
1142 unqualified : 'Designates that you do not have the qualifications necessary to work on the HIT',
1143 reqmaster : 'Designates HITs that require Masters',
1144 nomaster : 'Designates HITs that do not require Masters',
1145 defaultText : 'Default text color',
1146 inputText : 'Color of input boxes in the control panel',
1147 secondText : 'Color for text used on selected control panel items',
1148 link : 'Default color of unvisited links',
1149 vlink : 'Default color of visited links',
1150 export : 'Color of buttons in the results table--export and block buttons',
1151 hover : 'Color of control panel options on mouseover'
1152 };//}}}
1153 for (var r of _colors)
1154 dlbody.push(`<dt>${r.k}</dt><dd><div class="icbutt"><input data-key="${r.k}" type="color" value="${r.v}" /></div>${define(r.k)}</dd>`);
1155 this.node.innerHTML = '<b>THEME EDITOR</b><p></p><div style="height:87%;overflow:auto"><dl>' + dlbody.join('') + '</dl></div>' +
1156 '<button id="edSave" style="margin:5px auto;width:33%;color:white;background:black">Save</button>' +
1157 '<button id="edDefault" style="margin:5px auto;width:33%;color:white;background:black">Restore Default</button>' +
1158 '<button id="edCancel" style="margin:5px auto;width:33%;color:white;background:black">Cancel</button>';
1159 this.node.style.height = '57%';
1160 Array.from(this.node.querySelectorAll('.icbutt')).forEach(v => {
1161 v.style.background = v.firstChild.value;
1162 v.firstChild.onchange = e => {
1163 var k = e.target.dataset.key;
1164 v.style.background = e.target.value;
1165 _th.colors[_th.name][k] = e.target.value;
1166 Themes.apply(_th.name, Settings.user.hitColor);
1167 };
1168 });
1169 this.node.querySelector('#edDefault').onclick = () => {
1170 _th.colors[_th.name] = Themes.default[_th.name];
1171 Themes.apply(_th.name, Settings.user.hitColor);
1172 this.die();
1173 new Editor('theme');
1174 };
1175 this.node.querySelector('#edSave').onclick = () => {
1176 Settings.save();
1177 this.die();
1178 };
1179 break;
1180 case 'vbTemplate':
1181 this.node.innerHTML = '<b>VBULLETIN TEMPLATE</b><div style="float:right;margin-bottom:5px">Ratings Symbol: ' +
1182 `<input style="text-align:center" type="text" size="1" maxlength="1" value="${Settings.user.vbSym}" /></div>` +
1183 '<textarea style="display:block;height:200px;width:500px;font:12px monospace">' +
1184 Settings.user.vbTemplate + '</textarea>' +
1185 '<button id="edSave" style="margin:5px auto;width:33%;color:white;background:black">Save</button>' +
1186 '<button id="edDefault" style="margin:5px auto;width:33%;color:white;background:black">Restore Default</button>' +
1187 '<button id="edCancel" style="margin:5px auto;width:33%;color:white;background:black">Cancel</button>';
1188 this.node.querySelector('#edDefault').onclick = () => {
1189 this.node.querySelector('textarea').value = Settings.defaults.vbTemplate;
1190 this.node.querySelector('#edSave').click();
1191 };
1192 this.node.querySelector('#edSave').onclick = () => {
1193 Settings.user.vbTemplate = this.node.querySelector('textarea').value.trim();
1194 Settings.user.vbSym = this.node.querySelector('input').value;
1195 Settings.save();
1196 this.die();
1197 new Exporter({ target: this.caller });
1198 };
1199 break;
1200 }
1201 this.node.querySelector('#edCancel').onclick = () => this.die();
1202 },//}}}
1203
1204 Core = {//{{{
1205 active : false,
1206 timer : null,
1207 cooldown : null,
1208 lastScrape: null,
1209 getPayload: function(page=1) {//{{{
1210 const user = Settings.user,
1211 payload = {
1212 legacy: {
1213 searchWords : user.search,
1214 minReward : user.pay,
1215 qualifiedFor : Interface.isLoggedout ? 'off' : (user.qual ? 'on' : 'off'),
1216 requiresMasterQual: user.monly ? 'on' : 'off',
1217 sortType : '',
1218 pageNumber : page,
1219 pageSize : user.resultsPerPage || 50
1220 },
1221 next : {
1222 filters : {
1223 search_term: user.search,
1224 qualified : user.qual,
1225 masters : user.monly,
1226 min_reward : user.pay
1227 },
1228 page_size : user.resultsPerPage || 50,
1229 sort : '',
1230 format : 'json',
1231 page_number: page
1232 }
1233 };
1234 const sort = user.invert ? 'asc' : 'desc';
1235 switch (user.searchBy) {
1236 case 0:
1237 payload.legacy.sortType = `LastUpdatedTime:${+!user.invert}`;
1238 payload.next.sort = 'updated_' + sort;
1239 break;
1240 case 1:
1241 payload.legacy.sortType = `NumHITs:${+!user.invert}`;
1242 payload.next.sort = 'num_hits_' + sort;
1243 break;
1244 case 2:
1245 payload.legacy.sortType = `Reward:${+!user.invert}`;
1246 payload.next.sort = 'reward_' + sort;
1247 break;
1248 case 3:
1249 payload.legacy.sortType = `Title:${+user.invert}`;
1250 payload.next.sort = 'title_' + sort;
1251 break;
1252 }
1253 return payload;
1254 },//}}} Core::init
1255 run : function(skiptoggle) {//{{{
1256 if (!skiptoggle) this.active = !this.active;
1257 this.cooldown = +Settings.user.refresh;
1258 clearTimeout(this.timer);
1259 Interface.resetTitle();
1260 if (this.active) {
1261 const next = ENV.HOST === ENV.NEXT;
1262 const path = next ? '/' : '/mturk/searchbar';
1263 const resType = next ? 'json' : 'document';
1264 Interface.Status.push(' <b class="spinner"></b> Processing page: 1');
1265 this.fetch(path, this.getPayload(), resType);
1266 }
1267 },//}}} Core::run
1268 cruise : function() {//{{{
1269 if (!this.active) return;
1270 if (--this.cooldown === 0) this.run(true);
1271 else {
1272 Interface.Status.cd();
1273 this.timer = setTimeout(this.cruise.bind(this), 1000);
1274 }
1275 },//}}}
1276 dispatch : function(type, src) {//{{{
1277 switch (type) {
1278 case 'external':
1279 this.meld(src);
1280 break;
1281 case 'internal':
1282 if (ENV.HOST === ENV.LEGACY) {
1283 const error = src.querySelector('td[class="error_title"]');
1284 if (error && /page request/.test(error.textContent))
1285 return setTimeout(this.fetch.bind(this), 3000, src.documentURI);
1286 }
1287 this.scrape(src);
1288 break;
1289 case 'control':
1290 const blocked = scraperHistory.filter(v => v.current && v.blocked).length,
1291 _rpp = +Settings.user.resultsPerPage,
1292 skiplimit = 5,
1293 pagelimit = Settings.user.skips
1294 ? ((+Settings.user.pages + Math.floor(blocked / _rpp) + (blocked % _rpp > 0.66 * _rpp
1295 ? 1
1296 : 0)) || 3)
1297 : (+Settings.user.pages || 3);
1298
1299 if (!this.active || !src.nextPageURL || src.page >= pagelimit || (pagelimit - Settings.user.pages) >= skiplimit || (Interface.isLoggedout && src.page === 20)) {
1300 if (Settings.user.disableTO)
1301 this.meld();
1302 else {
1303 const ids = scraperHistory.filter(v => v.current && v.TO === null && v.requester.id, true)
1304 .filter((v, i, a) => a.indexOf(v) === i).join();
1305 if (!ids.length) return this.meld();
1306 Interface.Status.push(' <b class="spinner"></b> Retrieving TO data');
1307 this.fetch(TO_API + ids, null, 'json');
1308 }
1309 } else {
1310 Interface.Status.push(` <b class="spinner"></b> Processing page: ${+src.page + 1}`);
1311 if (+src.page + 1 > +Settings.user.pages) Interface.Status.append('; Correcting for skips');
1312 setTimeout(this.fetch.bind(this), 250, src.nextPageURL, src.payload, src.responseType);
1313 }
1314 break;
1315 }
1316 },//}}} Core::dispatch
1317 scrapeNext: function(src) {
1318 src.results.forEach((v, i) => {
1319 const data = {
1320 discovery: Date.now(),
1321 title: v.title,
1322 index: src.page_number + ('00' + i).slice(-2),
1323 requester: { name: v.requester_name, id: v.requester_id, link: ENV.ORIGIN + v.requester_url.replace(/\.json/, '') },
1324 pay: '$' + v.monetary_reward.amount_in_dollars.toFixed(2),
1325 payRaw: v.monetary_reward.amount_in_dollars,
1326 time: v.assignment_duration_in_seconds,
1327 desc: v.description,
1328 quals: v.project_requirements.length ? v.project_requirements.map(getQuals) : ['None'],
1329 hit: { preview: ENV.ORIGIN + v.project_tasks_url.replace(/\.json/, ''), panda: ENV.ORIGIN + v.accept_project_task_url.replace(/\.json/, '') },
1330 groupId: v.hit_set_id,
1331 TO: null,
1332 masters: !!~v.project_requirements.findIndex(q => q.qualification_type_id === '2F1QJWKUDD8XADTFD2Q0G6UTO95ALH'),
1333 numHits: v.assignable_hits_count,
1334 blocked: false,
1335 included: false,
1336 current: true,
1337 qualified: v.caller_meets_requirements,
1338 viable: !~v.project_requirements.findIndex(q => q.caller_meets_requirement === false && q.qualification_type.is_requestable === false)
1339 };
1340
1341 const listsxr = this.crossRef(data.requester.name, data.title); //check block/include lists
1342 data.blocked = listsxr[0];
1343 data.included = listsxr[1];
1344 if (Settings.user.searchBy === 1 && +Settings.user.batch > 1 && +data.numHits < +Settings.user.batch) return;
1345 else if (Settings.user.gbatch && +Settings.user.batch > 1 && +data.numHits < +Settings.user.batch) return;
1346 else if (Settings.user.onlyViable && !data.viable) return;
1347 scraperHistory.set(data.groupId, data);
1348 }, this);
1349
1350 const dispatchObj = {
1351 method: 'next',
1352 page: src.page_number,
1353 nextPageURL: src.num_results < Settings.user.resultsPerPage ? null : '/',
1354 payload: this.getPayload(src.page_number+1),
1355 responseType: 'json'
1356 };
1357 this.dispatch('control', dispatchObj);
1358
1359 function getQuals(qual) {
1360 return `${qual.qualification_type.name} ${qual.comparator} ${qual.qualification_values.join()}`;
1361 }
1362 },
1363 scrape : function(src) {//{{{
1364 if (ENV.HOST === ENV.NEXT) return this.scrapeNext(src);
1365 let page = +src.documentURI.match(/pageNumber=(\d+)/)[1],
1366 nextPageURL = src.querySelector('img[src="/media/right_arrow.gif"]'),
1367 titles = Array.from(src.querySelectorAll('a.capsulelink')),
1368 getCapsule = n => {
1369 for (let i = 0; i < 7; i++) n = n.parentNode;
1370 return n;
1371 };
1372 nextPageURL = nextPageURL ? nextPageURL.parentNode.href : null;
1373
1374 titles.forEach(function(v, i) {
1375 let capsule = getCapsule(v),
1376 get = q => capsule.querySelector(q),
1377 pad = n => ('00' + n).slice(-2),
1378 qualrows = Array.prototype.slice.call(get('a[id^="qualifications"]').parentNode.parentNode.parentNode.rows, 1),
1379 viable = true,
1380 capData = {
1381 discovery: Date.now(),
1382 title : v.textContent.trim(),
1383 index : page + pad(i),
1384 requester: { name: get('.requesterIdentity').textContent, id: null, link: null, linkTemplate: null },
1385 pay : get('span.reward').textContent,
1386 time : get('a[id^="duration"]').parentNode.nextElementSibling.textContent,
1387 desc : get('a[id^="description"]').parentNode.nextElementSibling.textContent,
1388 quals : qualrows.length
1389 ? qualrows.map(v => v.cells[0].textContent.trim().replace(/\s+/g, ' '))
1390 : ['None'],
1391 hit : { preview: null, previewTemplate: null, panda: null, pandaTemplate: null },
1392 groupId : null,
1393 TO : null,
1394 masters : null,
1395 numHits : null,
1396 blocked : false,
1397 included : false,
1398 current : true,
1399 qualified: !Boolean(get('a[href*="notqualified?"],a[id^="private_hit"]'))
1400 },
1401 listsxr = this.crossRef(capData.requester.name, capData.title); //check block/include lists
1402 capData.blocked = listsxr[0];
1403 capData.included = listsxr[1];
1404 capData.masters = /Masters/.test(capData.quals.join());
1405
1406 if (Interface.isLoggedout) {
1407 capData.TO = '';
1408 capData.qualified = false;
1409 capData.numHits = 'n/a';
1410 } else {
1411 viable = !qualrows.map(v => v.cells[2].textContent).filter(v => v.includes('do not')).length;
1412 capData.numHits = get('a[id^="number_of_hits"]').parentNode.nextElementSibling.textContent.trim();
1413 }
1414
1415 try { // groupid
1416 capData.groupId = get('a[href*="roupId="]').href.match(/[A-Z0-9]{30}/)[0];
1417 } catch(e) {
1418 void(e);
1419 capData.groupId = this.getHash(capData.requester.name + capData.title + capData.pay);
1420 }
1421 try { // requesterid, requester search link, groupid
1422 var _r = get('a[href*="requesterId"]');
1423 capData.requester.link = _r.href;
1424 capData.requester.id = _r.href.match(/[^=]+$/)[0];
1425 } catch(e) {
1426 void(e);
1427 capData.requester.link = '/mturk/searchbar?searchWords=' + window.encodeURIComponent(capData.requester.name);
1428 }
1429 try { // preview/panda links
1430 var _l = get('a[href*="preview?"]');
1431 capData.hit.preview = _l.href.split('?')[0] + '?groupId=' + capData.groupId;
1432 capData.hit.panda = capData.hit.preview.replace(/(\?)/, 'andaccept$1');
1433 } catch(e) {
1434 void(e);
1435 capData.hit.preview = 'https://www.mturk.com/mturk/searchbar?searchWords=' + window.encodeURIComponent(capData.title);
1436 }
1437
1438 if (Settings.user.searchBy === 1 && +Settings.user.batch > 1 && +capData.numHits < +Settings.user.batch) return;
1439 else if (Settings.user.gbatch && +Settings.user.batch > 1 && +capData.numHits < +Settings.user.batch) return;
1440 else if (Settings.user.onlyViable && !viable) return;
1441 scraperHistory.set(capData.groupId, capData);
1442 }, this);
1443
1444 this.dispatch('control', { method: 'legacy', page: page, nextPageURL: nextPageURL });
1445 },//}}} Core::scrape
1446 meld : function() {//{{{
1447 let reviews = arguments.length ? arguments[0] : null,
1448 table = document.querySelector('#resultsTable').tBodies[0], html = [], field, /*_gp, _gq,*/
1449 getClassFromValue = (val, type) => type === 'sim' ? (val > 4 ? 'toHigh' : (val > 3 ? 'toGood' : (val > 2
1450 ? 'toAverage'
1451 : 'toPoor')))
1452 : (val > 4.05 ? 'toHigh' : (val > 3.06 ? 'toGood' : (val > 2.4 ? 'toAverage' : (val > 1.7
1453 ? 'toLow'
1454 : 'toPoor')))),
1455 addRowHTML = r => {//{{{
1456 var _st = Interface.isLoggedout ? 'disabled' : '',
1457 _sh = ex => Settings.user['export' + ex] ? '' : 'hidden',
1458 _rt = r.blocked
1459 ? ''
1460 : `<div><button name="block" value="${r.requester.name}" style="width:15px" title="Block this requester">R</button>` +
1461 `<button name="block" value="${r.title.replace(/"/g, '"')}" style="width:15px" title="Block this title">T</button></div>`;
1462 return `<tr class="${r.included ? 'includelisted' : ''} ${shouldHide ? 'ignored hidden' : ''} ` +
1463 `${r.blocked ? 'blocklisted' : ''} ${r.rowColor} ${r.shine ? 'shine' : ''}">` +
1464 `<td>${_rt}<div><a class="static" target="_blank" href="${r.requester.link}">${r.requester.name}</a><div></td>` +
1465 `<td><div><button class="ex vb ${_st} ${_sh('Vb')}" style="width:30px" data-gid="${r.groupId}">vB</button>
1466 <button class="ex irc ${_st} ${_sh('Irc')}" style="width:30px" data-gid="${r.groupId}">IRC</button>
1467 <button class="ex hwtf ${_st} ${_sh('Hwtf')}" style="width:33px" data-gid="${r.groupId}">HWTF</button></div><div>
1468 <a title="Description: ${r.desc.replace(/"/g, '"')}\n\nQualifications: ${r.quals.join('; ')}" target="_blank" href="${r.hit.preview}">${r.title}</a>
1469 </div></td>` +
1470 `<td style="text-align:center"><a target="_blank" ${r.hit.panda
1471 ? 'href="' + r.hit.panda + '"'
1472 : ''}>${r.pay}</a></td>` +
1473 `<td style="text-align:center" >${r.numHits}</td>` +
1474 `<td style="text-align:center"><a class="static toLink" target="_blank" data-rid="${r.requester.id
1475 ? r.requester.id
1476 : 'null'}" ` +
1477 (r.requester.id ? 'href="' + TO_REPORTS + r.requester.id + '"' : '') + '>' +
1478 (r.TO ? r.TO.attrs.pay : 'n/a') + createTooltip('to', r.TO) + '</a></td>' +
1479 `<td class="${r.masters ? 'reqmaster' : 'nomaster'}" style="text-align:center">${r.masters
1480 ? 'Y'
1481 : 'N'}</td>` +
1482 `<td class="db nohitDB" data-index="requester${r.requester.id ? 'Id' : 'Name'}"
1483 data-value="${r.requester[r.requester.id ? 'id' : 'name']}" data-cmp-value="${r.title}"
1484 data-cmp-index="title" style="text-align:center;cursor:default">R</td>` +
1485 `<td class="db nohitDB" data-index="title" data-value="${r.title}" data-cmp-value="${r.requester.name}"
1486 data-cmp-index="requesterName" style="text-align:center;cursor:default">T</td>` +
1487 `${r.qualified ? '' : '<td class="tooweak" title="Not qualified to work on this HIT">NQ</td>'}` +
1488 '</tr>';
1489 },//}}}
1490 setRowColor = r => {
1491 var _t = Settings.user.colorType;
1492 if (!r.TO || r.TO.reviews < 5) {
1493 r.rowColor = 'toNone';
1494 return;
1495 }
1496 r.rowColor = getClassFromValue(_t === 'sim' ? r.TO.attrs.qual : r.TO.attrs.adjQual, _t);
1497 },
1498 bubbleNewHits = a => {
1499 var _new, _old = [];
1500 _new = a.filter(v => v.shine ? true : _old.push(v) && false);
1501 return _new.concat(_old);
1502 };
1503
1504 if (reviews) scraperHistory.updateTOData(prepReviews(reviews));
1505 let results = scraperHistory.filter(v => {
1506 if (!v.current) return false;
1507 v.current = false;
1508 if (Settings.user.mhide && v.masters) return false;
1509 else return true;
1510 });
1511
1512 // sorting
1513 if (!Interface.isLoggedout && !Settings.user.disableTO && Settings.user.sortPay !== Settings.user.sortAll) {
1514 if (Settings.user.sortPay)
1515 field = Settings.user.sortType === 'sim' ? 'pay' : 'adjPay';
1516 else if (Settings.user.sortAll)
1517 field = Settings.user.sortType === 'sim' ? 'qual' : 'adjQual';
1518
1519 results.sort((a, b) => {
1520 a = a.TO ? +a.TO.attrs[field] : 0;
1521 b = b.TO ? +b.TO.attrs[field] : 0;
1522 return b - a;
1523 });
1524 if (Settings.user.sortAsc) results.reverse();
1525 } else
1526 results.sort((a, b) => a.index - b.index);
1527
1528 // populating
1529 const counts = { total: results.length, new: 0, newVis: 0, ignored: 0, blocked: 0, included: 0, incNew: 0 };
1530 for (let r of (Settings.user.bubbleNew ? bubbleNewHits(results) : results)) {
1531 var shouldHide = Boolean((Settings.user.hideBlock && r.blocked) || (Settings.user.hideNoTO && !r.TO) ||
1532 (Settings.user.minTOPay && r.TO && +r.TO.attrs.pay < +Settings.user.minTOPay));
1533 counts.new += r.isNew ? 1 : 0;
1534 counts.newVis += r.isNew && !shouldHide ? 1 : 0;
1535 counts.ignored += shouldHide ? 1 : 0;
1536 counts.blocked += r.blocked ? 1 : 0;
1537 counts.included += r.included ? 1 : 0;
1538 counts.incNew += r.included && r.isNew ? 1 : 0;
1539 setRowColor(r);
1540 html.push(addRowHTML(r));
1541 }
1542 table.innerHTML = html.join('');
1543 this.notify(counts);
1544
1545 Array.from(table.querySelectorAll('.db')).forEach(v => HITStorage.test(v));
1546
1547 if (this.active) {
1548 if (this.cooldown === 0) Interface.buttons.main.click();
1549 else {
1550 this.timer = setTimeout(this.cruise.bind(this), 1000);
1551 Interface.Status.append(`<br />Scraping again in ${this.cooldown} seconds`);
1552 }
1553 }
1554 results = null;
1555 reviews = null;
1556 this.lastScrape = Date.now();
1557 },//}}}
1558 getHash : function(str) {//{{{
1559 var hash = 0, ch;
1560 for (var i = 0; i < str.length; i++) {
1561 ch = str.charCodeAt(i);
1562 hash = ch + (hash << 6) + (hash << 16) - hash;
1563 }
1564 return hash;
1565 },//}}} Core::getHash
1566 fetch : function(url, payload, responseType, inline) {//{{{
1567 const enc = window.encodeURIComponent;
1568 responseType = responseType || 'document';
1569 inline = inline === undefined ? true : inline;
1570 if (payload) {
1571 const key = ENV.HOST === ENV.NEXT ? 'next' : 'legacy';
1572 payload = payload[key];
1573 url += '?' + Object.entries(payload).map(stringify).join('&');
1574 }
1575
1576 function stringify(v) {
1577 const predicate = typeof v[1] !== 'string' && !(v[1] instanceof Array) ? Object.entries(v[1]) : '';
1578 if (predicate.length)
1579 return predicate.map(vp => (vp[0] = enc(`${v[0]}[${vp[0]}]`)) && vp) // 0 = o[i] => o%5Bi%5D
1580 .map(stringify)
1581 .join('&');
1582 return `${v[0]}=${enc(v[1])}`;
1583 }
1584
1585 const _p = new Promise(function(accept, rej) {
1586 const xhr = new XMLHttpRequest();
1587 xhr.open('GET', url, true);
1588 xhr.responseType = responseType;
1589 xhr.timeout = 6000;
1590 xhr.send();
1591 xhr.onload = function() {
1592 if (this.status === 200) accept(this.response);
1593 else rej(new Error(this.status + ' - ' + this.statusText));
1594 };
1595 xhr.onerror = function() {
1596 rej(new Error(this.status + ' - ' + this.statusText));
1597 console.log('error: ', this);
1598 };
1599 xhr.ontimeout = function() {
1600 rej(new Error('Request timed out - ' + url));
1601 console.log('timeout: ', this);
1602 };
1603 });
1604 const source = url.split('?')[0].includes('turkopticon') ? 'external' : 'internal';
1605 if (inline) _p.then(this.dispatch.bind(this, source), err => {
1606 console.warn(err);
1607 this.meld.apply(this);
1608 });
1609 else return _p;
1610 },//}}} Core::fetch
1611 crossRef : function(...needles) {//{{{
1612 var found = [false, false], s;
1613 if (Settings.user.onlyIncludes) { // everything not in includelist gets blocked, unless includelist is empty or doesn't exist
1614 var list = (localStorage.getItem('scraper_include_list') || '').toLowerCase().split('^');
1615 if (list.length === 1 && !list[0].length) return found; // includelist is empty
1616 for (s of needles) {
1617 found[1] = Boolean(~list.indexOf(s.toLowerCase().replace(/\s+/g, ' ')));
1618 if (found[1]) {
1619 found[0] = false;
1620 break;
1621 } else
1622 found[0] = true;
1623 }
1624 return found;
1625 } else {
1626 if (localStorage.getItem('scraper_ignore_list') === null) new Editor().setDefaultBlocks();
1627 var blist = (localStorage.getItem('scraper_ignore_list') || '').toLowerCase().split('^'),
1628 ilist = (localStorage.getItem('scraper_include_list') || '').toLowerCase().split('^'),
1629 blist_wild = Settings.user.wildblocks ? blist.filter(v => /.*?[*].*/.test(v)) : null;
1630 if (blist_wild) blist_wild.forEach((v, i, a) =>
1631 a[i] = new RegExp('^' + (v.replace(/([+${}[\](\)^|?.\\])/g, '\\$1') // escape non wildcard special chars
1632 .replace(/([^*]|^)[*](?!\*)/g, '$1.*') // turn
1633 // glob
1634 // into
1635 // regex
1636 .replace(/\*{2,}/g, s => s.replace(/\*/g, '\\$&'))) + '$'), 'i'); // escape consecutive asterisks
1637 for (s of needles) {
1638 found[0] = found[0] || Boolean(~blist.indexOf(s.toLowerCase().replace(/\s+/g, ' ')));
1639 found[1] = found[1] || Boolean(~ilist.indexOf(s.toLowerCase().replace(/\s+/g, ' ')));
1640 if (blist_wild && blist_wild.length && !found[0])
1641 for (var i = 0; !found[0] && i < blist_wild.length; i++) found[0] = blist_wild[i].test(s.toLowerCase().replace(/\s+/g, ' '));
1642 }
1643 return found; // [ blocklist,includelist ]
1644 }
1645 },//}}} Core::crossRef
1646 notify : function(c) {//{{{
1647 var s = ['Scrape Complete: '];
1648 s.push(c.total > 0 ? `${c.total} HIT${c.total > 1 ? 's' : ''}` : '<b>No HITs found.</b>');
1649 if (c.new) s.push(`<i></i>${c.new} new`);
1650 if (c.newVis !== c.new) s.push(` (${c.newVis} shown)`);
1651 if (c.included) s.push(`<i></i><b>${c.included} from includelist</b>`);
1652 if (c.ignored) s.push(`<i></i>${c.ignored} hidden -- `);
1653 if (c.blocked) s.push(`${c.ignored ? '' : '<i></i>'}${c.blocked} from blocklist`);
1654 if (c.ignored - c.blocked > 0) s.push(`${c.blocked
1655 ? '<i></i>'
1656 : ''}${c.ignored - c.blocked} below TO threshold`);
1657 Interface.Status.push(s.join(''));
1658
1659 if (c.newVis && Settings.user.notifySound[0]) document.getElementById(Settings.user.notifySound[1]).play();
1660 if (!c.newVis || Interface.focused) return;
1661 document.title = `[${c.newVis} new]` + DOC_TITLE;
1662 if (Settings.user.notifyBlink) Interface.blackhole.blink =
1663 setInterval(() => document.title = /scraper/i.test(document.title)
1664 ? `${c.newVis} new HITs`
1665 : DOC_TITLE, 1000);
1666 if (Settings.user.notifyTaskbar && Notification.permission === 'granted') {
1667 var inc = c.incNew ? ` (${c.incNew} from includelist)` : '',
1668 n = new Notification('HITScraper found ' + c.newVis + ' new HITs' + inc);
1669 n.onclick = n.close;
1670 setTimeout(n.close.bind(n), 5000);
1671 }
1672 }//}}} Core::notify
1673 },//}}} Core
1674
1675 Exporter = function(e) {//{{{
1676 Interface.toggleOverflow('on');
1677 this.caller = e.target;
1678 this.node = document.body.appendChild(document.createElement('DIV'));
1679 this.node.classList.add('pop');
1680 this.die = () => {
1681 Interface.toggleOverflow('off');
1682 this.node.remove();
1683 };
1684 this.record = scraperHistory.get(this.caller.dataset.gid);
1685
1686 if (Interface.isLoggedout) return this.die();
1687
1688 var _vb = () => {//{{{
1689 var
1690 getColor = attr => {
1691 switch (attr) {
1692 case 5:
1693 case 4:
1694 return 'green';
1695 case 3:
1696 return 'yellow';
1697 case 2:
1698 return 'orange';
1699 case 1:
1700 return 'red';
1701 default:
1702 return 'white';
1703 }
1704 },
1705 templateVars = {//{{{
1706 title : this.record.title,
1707 requesterName: this.record.requester.name,
1708 requesterLink: this.record.requester.link,
1709 requesterId : this.record.requester.id,
1710 description : this.record.desc,
1711 reward : this.record.pay,
1712 quals : this.record.quals.join(';').replace(/(;?)(\w* ?Masters.+?)(;?)/g, '$1[COLOR=red][b]$2[/b][/COLOR]$3'),
1713 previewLink : this.record.hit.preview,
1714 pandaLink : this.record.hit.panda,
1715 time : this.record.time,
1716 numHits : this.record.numHits,
1717 toImg : '', // deprecated
1718 toCompact : (function() {//{{{
1719 var _to = this.record.TO, txt = ['[b]'], color;
1720 if (!_to) return 'TO Unavailable';
1721 for (var a of ['comm', 'pay', 'fair', 'fast']) {
1722 color = getColor(Math.floor(_to.attrs[a]));
1723 txt.push(`[ ${a}: [COLOR=${color}]${_to.attrs[a]}[/COLOR] ]`);
1724 }
1725 return txt.join('') + '[/b]';
1726 }).apply(this),//}}} toCompact
1727 toVerbose : (function() {//{{{
1728 var _to = this.record.TO, txt = [], color, _attr, sym = Settings.user.vbSym,
1729 _long = { comm: 'Communicativity', pay: 'Generosity', fair: 'Fairness', fast: 'Promptness' };
1730 if (!_to) return 'TO Unavailable';
1731 for (var a of ['comm', 'pay', 'fair', 'fast']) {
1732 _attr = Math.floor(_to.attrs[a]);
1733 color = getColor(_attr);
1734 txt.push((_attr > 0 ? (`[COLOR=${color}]${sym.repeat(_attr)}[/COLOR]` + (_attr < 5
1735 ? `[COLOR=white]${sym.repeat(5 - _attr)}[/COLOR]`
1736 : ''))
1737 : '[COLOR=white]' + sym.repeat(5) + '[/COLOR]') + ` ${_to.attrs[a]} ${_long[a]}`);
1738 }
1739 return txt.join('\n');
1740 }).apply(this),//}}} toText
1741 toFoot : (function() {//{{{
1742 var _to = this.record.TO,
1743 payload = `requester[amzn_id]=${this.record.requester.id}&requester[amzn_name]=${this.record.requester.name}`,
1744 newReview = `[URL="${TO_BASE + 'report?' + payload}"]Submit a new TO review[/URL]`;
1745 if (!_to) return newReview;
1746 return `Number of Reviews: ${_to.reviews} | TOS Flags: ${_to.tos_flags}\n` + newReview;
1747 }).apply(this)//}}} toFoot
1748 },//}}} templateVars obj
1749 createTemplate = function(str) {
1750 /*jshint -W054*/ // ignore evil due to required eval (function constructor)
1751 // TODO: find a concise way to dynamically generate a template without using eval
1752 var _str = str.replace(/\$\{ *([-\w\d.]+) *\}/g, (_, p1) => `\$\{vars.${p1}\}`);
1753 return new Function('vars', `try {return \`${_str}\`} catch(e) {return "Error in template: "+e.message}`);
1754 };
1755 templateVars.toText = templateVars.toVerbose; // temporary backwards compatibility
1756 this.node.innerHTML = '<p>vB Export</p>' +
1757 '<textarea style="display:block;padding:2px;margin:auto;height:250px;width:500px" tabindex="1">' +
1758 createTemplate(Settings.user.vbTemplate)(templateVars) + '</textarea>' +
1759 '<button id="exTemplate" style="margin-top:5px;width:50%;color:white;background:black">Edit Template</button>' +
1760 '<button id="exClose" style="margin-top:5px;width:50%;color:white;background:black">Close</button>';
1761 this.node.querySelector('#exTemplate').onclick = () => {
1762 this.die();
1763 new Editor('vbTemplate', this.caller);
1764 };
1765 this.node.querySelector('#exClose').onclick = this.die;
1766 this.node.querySelector('textarea').select();
1767 },//}}}
1768 _irc = () => {//{{{
1769 // custom MTurk/TO url shortener courtesy of Tjololo
1770 var api = 'https://ns4t.net/yourls-api.php?action=bulkshortener&title=MTurk&signature=39f6cf4959',
1771 urlArr = [], payload, sym = '\u2022', // sym = bullet
1772 getTO = () => {
1773 var _to = this.record.TO;
1774 if (!_to) return 'Unavailable';
1775 else return `Pay=${_to.attrs.pay} Fair=${_to.attrs.fair} Comm=${_to.attrs.comm}`;
1776 };
1777
1778 urlArr.push(window.encodeURIComponent(this.record.requester.link));
1779 urlArr.push(window.encodeURIComponent(this.record.hit.preview));
1780 urlArr.push(window.encodeURIComponent(TO_REPORTS + this.record.requester.id));
1781 urlArr.push(window.encodeURIComponent(this.record.hit.panda));
1782 payload = '&urls[]=' + urlArr.join('&urls[]=');
1783
1784 this.node.innerHTML = '<span style="font-size:16px">Shortening URLs... <i class="spinner"></i></span>';
1785 Core.fetch(api + payload, null, 'text', false).then(r => {
1786 urlArr = r.split(';').slice(0, 4);
1787 this.node.innerHTML = '<p>IRC Export</p>' +
1788 '<textarea style="display:block;padding:2px;margin:auto;height:130px;width:500px" tabindex="1">' +
1789 (/masters/i.test(this.record.quals.join()) ? `MASTERS ${sym} ` : '') +
1790 `Requester: ${this.record.requester.name} ${urlArr[0]} ${sym} HIT: ${this.record.title} ` +
1791 `${urlArr[1]} ${sym} Pay: ${this.record.pay} ${sym} Avail: ${this.record.numHits} ${sym} ` +
1792 `Limit: ${this.record.time} ${sym} TO: ${getTO()} ${urlArr[2]} ${sym} PandA: ${urlArr[3]}</textarea>` +
1793 '<button id="exClose" style="width:100%;padding:5px;margin-top:5px;background:black;color:white">Close</button>';
1794 this.node.querySelector('textarea').select();
1795 this.node.querySelector('#exClose').onclick = this.die;
1796 }, err => {
1797 console.error(err);
1798 this.die();
1799 });
1800 },//}}}
1801 _hwtf = () => {//{{{
1802 var _location = 'ICA', _quals, _masters = '', _title, _r = this.record, tIndex;
1803 // format qualifications string
1804 _quals = _r.quals.map(v => {
1805 if (/(is US|: US$)/.test(v))
1806 _location = 'US';
1807 else if (/Masters/.test(v))
1808 _masters = `[${v.match(/.*Masters/)[0].toUpperCase()}]`;
1809 else if (/approv[aled]+ (rate|HITs)/.test(v))
1810 return v.replace(/.+ is (.+) than (\d+)/, (_, p1, p2) => {
1811 if (/^(not g|less)/.test(p1)) return '<' + p2 + (/%/.test(_) ? '%' : '');
1812 else if (/^(not l|greater)/.test(p1)) return '>' + p2 + (/%/.test(_) ? '%' : '');
1813 else console.error('match error', [_, p1, p2]);
1814 return _;
1815 });
1816 else
1817 return v;
1818 }).filter(v => v).sort(a => /[><]/.test(a) ? -1 : 1);
1819 _title = `${_location} - ${_r.title} - ${_r.requester.name} - ${_r.pay}/COMTIME - (${_quals.join(', ') || 'None'}) ${_masters}`;
1820 tIndex = _title.search(/COMTIME/);
1821 this.node.style.whiteSpace = 'nowrap';
1822 this.node.innerHTML = '<p style="width:500px;white-space:normal">' +
1823 '/r/HitsWorthTurkingFor Export: Use the buttons on the left for single-click copying. ' +
1824 'Before you post, please remember to replace "COMTIME" with how long it took you to complete the HIT.</p>' +
1825 '<button class="exhwtf" style="height:65px">Title</button>' +
1826 '<textarea style="padding:2px;margin:auto;height:60px;width:430px;resize:none" tabindex="1" autofocus>' +
1827 _title + '</textarea><br />' + '<button class="exhwtf" style="height:35px">Preview</button>' +
1828 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="2">' +
1829 'Preview: ' + _r.hit.preview + '</textarea><br />' + '<button class="exhwtf" style="height:35px;">Req</button>' +
1830 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="3">' +
1831 'Req: ' + _r.requester.link + '</textarea><br />' + '<button class="exhwtf" style="height:35px;">PandA</button>' +
1832 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="4">' +
1833 'PandA: ' + _r.hit.panda + '</textarea><br />' + '<button class="exhwtf" style="height:35px;">TO</button>' +
1834 '<textarea style="padding:2px;margin:auto;height:30px;width:430px;resize:none" tabindex="5">' +
1835 'TO: ' + TO_REPORTS + _r.requester.id + '</textarea><br />' +
1836 '<button id="exClose" style="width:100%;padding:5px;margin-top:5px;background:black;color:white">Close</button>';
1837
1838 var copyfn = function(e) {
1839 e.target.nextSibling.select();
1840 document.execCommand('copy');
1841 };
1842 Array.from(this.node.querySelectorAll('.exhwtf')).forEach(v => v.onclick = copyfn);
1843 this.node.querySelector('#exClose').onclick = this.die;
1844 this.node.querySelector('textarea').setSelectionRange(tIndex, tIndex + 7);
1845 };//}}}
1846
1847 switch (this.caller.textContent.toLowerCase()) {
1848 case 'vb':
1849 _vb();
1850 break;
1851 case 'irc':
1852 _irc();
1853 break;
1854 case 'hwtf':
1855 _hwtf();
1856 break;
1857 }
1858 },//}}} Exporter
1859
1860 HITStorage = {//{{{
1861 db : null,
1862 attach: function(name) {//{{{
1863 var dbh = window.indexedDB.open(name);
1864 dbh.onversionchange = e => {
1865 e.target.result.close();
1866 console.info('DB connection closed by external source');
1867 };
1868 dbh.onsuccess = e => this.db = e.target.result;
1869 },//}}} HITStorage::attach
1870 test : function(node) {//{{{
1871 if (!this.db || !this.db.objectStoreNames.contains('HIT')) return;
1872 this.db.transaction('HIT', 'readonly').objectStore('HIT').index(node.dataset.index).get(node.dataset.value)
1873 .onsuccess = e => { if (e.target.result) node.className = node.className.replace(/no/, ''); };
1874 },//}}} HITStorage::test
1875 query : function(node) {//{{{
1876 var range = window.IDBKeyRange.only(node.dataset.value), results = [];
1877 return new Promise((a, r) => {
1878 if (!this.db || !this.db.objectStoreNames.contains('HIT')) r(0);
1879 this.db.transaction('HIT', 'readonly').objectStore('HIT').index(node.dataset.index).openCursor(range)
1880 .onsuccess = e => {
1881 if (e.target.result) {
1882 results.push(e.target.result.value);
1883 e.target.result.continue();
1884 } else
1885 a(results.sort((a, b) => a.date > b.date ? 1 : -1));
1886 };
1887 });
1888 }//}}} HITStorage::query
1889 },//}}} HITStorage
1890
1891 FileHandler = {//{{{
1892 exports: function() {//{{{
1893 var obj = {
1894 settings : JSON.stringify(Settings.user),
1895 ignore_list : localStorage.getItem('scraper_ignore_list') || '',
1896 include_list: localStorage.getItem('scraper_include_list') || ''
1897 },
1898 blob = new Blob([JSON.stringify(obj)], { type: 'application/json' }),
1899 a = document.body.appendChild(document.createElement('a'));
1900 a.href = URL.createObjectURL(blob);
1901 a.download = 'hitscraper_settings.json';
1902 a.click();
1903 a.remove();
1904 },//}}}
1905 imports: function(e) {//{{{
1906 var f = e.target.files,
1907 invalid = () => Settings.main.querySelector('#eisStatus').textContent = 'Invalid file.';
1908 if (!f.length) return;
1909 if (!f[0].name.includes('json')) return invalid();
1910 var reader = new FileReader();
1911 reader.readAsText(f[0]);
1912 reader.onload = function() {
1913 var obj;
1914 try { obj = JSON.parse(this.result); } catch(err) { return invalid(); }
1915 for (var key of ['settings', 'ignore_list', 'include_list']) {
1916 if (key in obj && typeof obj[key] === 'string')
1917 localStorage.setItem('scraper_' + key, obj[key]);
1918 }
1919 initialize();
1920 };
1921 }//}}}
1922 };//}}} FileHandler
1923
1924 function initialize() {//{{{
1925 Settings.user = Object.assign({}, Settings.defaults, JSON.parse(localStorage.getItem('scraper_settings')));
1926 Interface.draw().init();
1927 scraperHistory = new ScraperCache(650);
1928 }//}}}
1929
1930 function createTooltip(type, obj) {//{{{
1931 var html, bullet = li => `<ul><li>${li}</li></ul>`,
1932 reason = Settings.user.disableTO ? bullet('TO disabled in user settings')
1933 : (Interface.isLoggedout ? bullet('Cannot retrieve TO while logged out')
1934 : (obj === '' ? bullet('Requester has not been reviewed yet') : bullet('Invalid response from server'))),
1935 _genMeters = function() {
1936 var attrmap = { comm: 'Communicativity', pay: 'Generosity', fair: 'Fairness', fast: 'Promptness' }, html = [];
1937 for (var k in attrmap) {
1938 if (attrmap.hasOwnProperty(k)) {
1939 html.push(`<meter min="0.8" low="2.5" high="3.4" optimum="5" max="5" value=${obj.attrs[k]} data-attr=${attrmap[k]}></meter>`);
1940 }
1941 }
1942 if (ENV.ISFF) // firefox is shitty and doesn't support ::after/::before pseudo-elements on meter elements
1943 html.forEach((v, i, a) => a[i] = '<div style="position:relative">' + v +
1944 `<span class="ffmb">${attrmap[Object.keys(attrmap)[i]]}</span>` +
1945 `<span class="ffma">${obj.attrs[Object.keys(attrmap)[i]]}</span></div>`);
1946 return html.join('');
1947 };
1948
1949 if (!obj) {
1950 html = `<div class="tooltip" style="width:260px;"><p style="padding-left:5px">Turkopticon data unavailable:${reason}</p></div>`;
1951 } else if (type === 'to')
1952 html = `<div class="tooltip" style="width:260px">
1953 <p style="padding-left:5px"><b>${obj.name}</b><br />Reviews: ${obj.reviews} | TOS Flags: ${obj.tos_flags}</p>
1954 ${_genMeters()}</div>`;
1955 /*<table style="margin-top:6px;width:100%;font-size:10px"><tr><td>Adjusted Pay</td><td>${obj.attrs.adjPay}</td>
1956 <td>${getClassFromValue(obj.attrs.adjPay, 'adj').slice(2)}</td></tr><tr><td>Weighted Score</td><td>${obj.attrs.qual}</td>
1957 <td>${getClassFromValue(obj.attrs.qual, 'sim').slice(2)}</td></tr><tr><td>Adjusted Score</td><td>${obj.attrs.adjQual}</td>
1958 <td>${getClassFromValue(obj.attrs.adjQual, 'adj').slice(2)}</td></tr></table></div>;*/
1959 else // XXX not used atm
1960 html = `<div class="tooltip" style="width:300px"><dl><dt>description</dt><dd>${obj.desc}</dd>
1961 <dt>qualifications</dt><dd>${obj.quals}</dd></dl>`;
1962
1963 return html;
1964 }//}}}
1965
1966 function prepReviews(reviews) {
1967 const adj = (x, n) => ((x * n + 15) / (n + 5)) - 1.645 * Math.sqrt((Math.pow(1.0693 * x, 2) - Math.pow(x, 2)) / (n + 5));
1968 Object.keys(reviews).forEach(rid => {
1969 if (typeof reviews[rid] === 'string') return delete reviews[rid]; // no reviews yet
1970
1971 //adjust ratings
1972 let n = 0, d = 0;
1973 Object.keys(reviews[rid].attrs).forEach(attr => {
1974 n += reviews[rid].attrs[attr] * Settings.user.toWeights[attr];
1975 d += +Settings.user.toWeights[attr];
1976 });
1977 reviews[rid].attrs.qual = (n / d).toPrecision(4);
1978 reviews[rid].attrs.adjQual = adj(n / d, +reviews[rid].reviews).toPrecision(4);
1979 reviews[rid].attrs.adjPay = adj(+reviews[rid].attrs.pay, +reviews[rid].reviews).toPrecision(4);
1980 });
1981 return reviews;
1982 }
1983
1984 class Cache {
1985 constructor(limit = 500) {
1986 this.limit = limit;
1987 this._length = 0;
1988 this._cache = Object.create(null);
1989 this._tmp = Object.create(null);
1990 }
1991
1992 get(key) {
1993 let val = this._cache[key];
1994 if (val)
1995 return val;
1996 else if ((val = this._tmp[key]))
1997 return this._update(key, val);
1998 else
1999 return null;
2000 }
2001
2002 set(key, value) {
2003 if (this._cache[key])
2004 return (this._cache[key] = value);
2005 else
2006 this._update(key, value);
2007 }
2008
2009 has(key) {
2010 return !!this.get(key);
2011 }
2012
2013 _update(key, value) {
2014 this._cache[key] = value;
2015 if (++this._length > this.limit) {
2016 this._length = 0;
2017 this._tmp = this._cache;
2018 this._cache = Object.create(null);
2019 }
2020 return value;
2021 }
2022 }
2023
2024 class ScraperCache extends Cache {
2025 constructor(limit = 500) {
2026 super(limit);
2027 this._toCache = new TOCache();
2028 }
2029
2030 set(key, value) {
2031 const first = !Core.lastScrape;
2032 if (this.get(key)) { // exists
2033 const age = Math.floor((Date.now() - this._cache[key].discovery) / 1000),
2034 obj = { isNew: false, shine: !!(this._cache[key].shine && age < +Settings.user.shine && !first) };
2035 value.discovery = this._cache[key].discovery;
2036 return (this._cache[key] = Object.assign(value, obj));
2037 } else { // new
2038 const obj = { isNew: !first, shine: !first, TO: this._toCache.get(value.requester.id) };
2039 this._update(key, Object.assign(value, obj));
2040 }
2041 }
2042
2043 filter(callback, rids = false) {
2044 const results = [], keys = Object.keys(this._cache);
2045 Object.keys(this._cache).forEach(key => {
2046 const val = this.get(key);
2047 if (callback(val, key, this._cache))
2048 results.push(rids ? val.requester.id : val);
2049 });
2050 return results;
2051 }
2052
2053 updateTOData(reviews) {
2054 this._toCache.setBatch(reviews);
2055 this.filter(v => v.current && v.TO === null).forEach(group => {
2056 if (this._toCache.has(group.requester.id))
2057 this._cache[group.groupId].TO = Object.assign(this._toCache.get(group.requester.id), { name: group.requester.name });
2058 });
2059 }
2060 }
2061
2062 class TOCache extends Cache {
2063 setBatch(reviews) {
2064 if (!reviews) return null;
2065 Object.keys(reviews).forEach(rid => this._update(rid, reviews[rid]));
2066 return reviews;
2067 }
2068 }
2069
2070 const kb = { ESC: 27, ENTER: 13 };
2071
2072 function Dialogue(caller) {//{{{
2073 Interface.toggleOverflow('on');
2074 this.node = document.body.appendChild(document.createElement('DIV'));
2075 this.die = () => {
2076 Interface.toggleOverflow('off');
2077 this.node.remove();
2078 };
2079 this.node.style.cssText = 'position:fixed;z-index:20;top:15%;left:50%;width:320px;padding:20px;transform:translate(-50%);' +
2080 'background:#000;color:#fff;box-shadow:0px 0px 6px 1px #fff';
2081 var target = caller.textContent === 'R' ? 'requester' : 'title';
2082 this.node.innerHTML = `<p><b>Add this ${target} to the blocklist?</b></p><p>"${caller.value}"</p>
2083 <div style="text-align:right;margin-right:30px;margin-top:10px;padding-top:10px">
2084 <button id="confirm" style="font-weight:bold;padding:7px;width:65px">OK</button>
2085 <button id="cancel" style="padding:7px;width:65px;">Cancel</button></div>`;
2086 this.node.querySelector('#confirm').onclick = () => {
2087 var bl = localStorage.getItem('scraper_ignore_list'), bstr = caller.value.toLowerCase().replace(/\s+/g, ' ');
2088 if (!bl) bl = bstr;
2089 else if (bl.slice(-1) === '^') bl += bstr;
2090 else bl += '^' + bstr;
2091 localStorage.setItem('scraper_ignore_list', bl);
2092
2093 Array.prototype.forEach.call(document.getElementById('resultsTable').tBodies[0].rows, v => {
2094 var c0 = v.cells[0].lastChild.textContent, c1 = v.cells[1].lastChild.textContent.trim();
2095 if (v.classList.contains('blocklisted') || c0 !== caller.value && c1 !== caller.value) return;
2096 v.cells[0].firstChild.remove();
2097 return v.classList.add('blocklisted') || Settings.user.hideBlock && v.classList.add('hidden');
2098 });
2099 this.die();
2100 };
2101 this.node.querySelector('#cancel').onclick = this.die;
2102 this.node.addEventListener('keydown', e => {
2103 if (e.keyCode === kb.ESC)
2104 this.die();
2105 }, true);
2106 this.node.querySelector('#confirm').focus();
2107 }//}}}
2108
2109 function DBQuery(node) {//{{{
2110 Interface.toggleOverflow('on');
2111 this.node = document.body.appendChild(document.createElement('DIV'));
2112 this.die = () => {
2113 this.node.remove();
2114 Interface.toggleOverflow('off');
2115 };
2116 this.node.style.cssText = 'position:fixed;z-index:20;top:50%;left:50%;padding:8px;' +
2117 'background:#fff;color:#000;box-shadow:0px 0px 6px 1px #bfbfbf;transform:translate(-50%,-50%);';
2118 this.node.innerHTML = '<div style="text-align:center;font-size:16px;"><p><b>Querying database... <i class="spinner"></i></b></p></div>';
2119 HITStorage.query(node).then(r => {
2120 var _tbody = [], _tfoot, t = { hits: 0, app: 0, rej: 0, pen: 0 },
2121 _thead = '<tr style="background:lightgrey;color:black"><th style="width:90px;padding:5px">Date</th>' +
2122 '<th style="width:120px">Requester</th><th>Title</th><th>Pay</th><th>Bonus</th><th>Status</th><th>Feedback</th></tr>',
2123 html = '<div style="position:absolute;top:0;left:0;margin:0;text-align:right;padding:0px;border:none;width:100%">' +
2124 '<label id="close" class="close" title="Close"> ✘ </label></div>';
2125 if (!r.length)
2126 html += `<h2>Nothing found matching "${node.dataset.value}"</h2>`;
2127 else {
2128 r.forEach((v, i) => {
2129 var _pay, _bonus, _sc, _bg;
2130 if (typeof v.reward === 'object') {
2131 _pay = '$' + v.reward.pay.toFixed(2);
2132 _bonus = v.reward.bonus > 0 ? '$' + v.reward.bonus.toFixed(2) : '';
2133 } else {
2134 _pay = '$' + v.reward.toFixed(2);
2135 _bonus = '';
2136 }
2137
2138 _sc = /(paid|approved)/i.test(v.status) ? 'green' : (/approval/i.test(v.status) ? 'orange' : 'red');
2139 _bg = v[node.dataset.cmpIndex] === node.dataset.cmpValue ? 'lightgreen' : (i % 2 ? '#F1F3EB' : '#fff');
2140 _tbody.push(`<tr style="background:${_bg}">
2141 <td>${v.date}</td><td>${v.requesterName}</td><td>${v.title}</td><td>${_pay}</td><td>${_bonus}</td>
2142 <td style="color:${_sc}">${v.status}</td><td>${v.feedback}</td></tr>`);
2143 t.hits++;
2144 t.app += /(paid|approved)/i.test(v.status) ? +_pay.slice(1) : 0;
2145 t.rej += /rejected/i.test(v.status) ? +_pay.slice(1) : 0;
2146 t.pen += /approval/i.test(v.status) ? +_pay.slice(1) : 0;
2147 });
2148 _tfoot = `<tr style="background:lightgrey;text-align:center"><td colspan="7">${t.hits} HITs: $${t.app.toFixed(2)} approved,
2149 $${t.pen.toFixed(2)} pending, $${t.rej.toFixed(2)} rejected</td>`;
2150 html += `<div style="margin-top:20px;width:100%;height:calc(100% - 20px);overflow:auto">
2151 <table style="border:1px solid black;border-collapse:collapse;width:100%">
2152 <thead>${_thead}</thead><tbody>${_tbody.join('')}</tbody><tfoot>${_tfoot}</tfoot></table></div>`;
2153 }
2154 this.node.style.cssText += `width:85%;${r.length ? 'height:85%;' : 'max-height:85%;'}`;
2155 this.node.innerHTML = html;
2156 this.node.querySelector('#close').onclick = this.die;
2157 }, () => this.die());
2158 }//}}}
2159
2160 // helpers
2161 function on(target, type, handler) { target.addEventListener(type, handler); }
2162
2163 function delegate(target, selector, type, handler) {
2164 function dispatcher(event) {
2165 const targets = target.querySelectorAll(selector);
2166 let i = targets.length;
2167
2168 while (i--) {
2169 if (event.target === targets[i]) {
2170 handler(event);
2171 break;
2172 }
2173 }
2174 }
2175
2176 on(target, type, dispatcher);
2177 }
2178
2179 Object.entries = Object.entries || function(obj) {
2180 const props = Object.keys(obj);
2181 let i = props.length;
2182 const objArray = new Array(i);
2183 while (i--) objArray[i] = [props[i], obj[props[i]]];
2184 return objArray;
2185 };
2186
2187 // event handlers
2188 function tomouseover(e) {
2189 e.target.children[0].style.display = 'block';
2190 const tt = e.target.children[0], rect = tt.getBoundingClientRect();
2191 if (rect.height > (window.innerHeight - e.clientY)) tt.style.transform = 'translateY(calc(-100% + 22px))';
2192 }
2193
2194 function tomouseout(e) {
2195 const tt = e.target.querySelector('.tooltip');
2196 if (!tt) return;
2197 tt.style.transform = '';
2198 tt.style.display = 'none';
2199 }
2200
2201 // ep
2202 console.log('HS hook');
2203 if (document.getElementById('control_panel')) {
2204 if (confirm('Another version of HITScraper was detected and has already claimed this page. Open HITScraper in a new tab?'))
2205 window.open('https://www.mturk.com/mturk/findhits?match=true?hit_scraper-dev');
2206 } else {
2207 initialize();
2208 HITStorage.attach('HITDB');
2209 const rt = document.getElementById('resultsTable');
2210 delegate(rt, 'tr:not(hidden) .toLink', 'mouseover', tomouseover);
2211 delegate(rt, 'tr:not(hidden) .toLink', 'mouseout', tomouseout);
2212 delegate(rt, 'tr:not(hidden) .ex', 'click', e => new Exporter(e));
2213 delegate(rt, 'tr:not(hidden) button[name=block]', 'click', ({ target }) => new Dialogue(target));
2214 delegate(rt, 'tr:not(hidden) .db', 'click', ({ target }) => new DBQuery(target));
2215 }
2216
2217})();
2218
2219// vim: ts=2:sw=2:et:fdm=marker:noai