Extra Flags for int

Extra Flags for int v2 "City flags were a mistake" edition

  1. // ==UserScript==
  2. // name and namespace cannot be changed - it would break the update mechanism, that's why we will leave the name at Extra Flags for int
  3. // @name Extra Flags for int
  4. // @namespace com.whatisthisimnotgoodwithcomputers.extraflagsforint
  5. // @description Extra Flags for int v2 "City flags were a mistake" edition
  6. // @include http*://boards.4chan.org/int/*
  7. // @include http*://boards.4chan.org/sp/*
  8. // @include http*://boards.4chan.org/pol/*
  9. // @include http*://boards.4chan.org/bant/*
  10. // @exclude http*://boards.4chan.org/int/catalog
  11. // @exclude http*://boards.4chan.org/sp/catalog
  12. // @exclude http*://boards.4chan.org/pol/catalog
  13. // @exclude http*://boards.4chan.org/bant/catalog
  14. // @version 0.29
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_getValue
  18. // @grant GM_setValue
  19. // @grant GM_addStyle
  20. // @run-at document-end
  21. // ==/UserScript==
  22.  
  23. // DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR REGION SHOULD BE CONFIGURED BY USING THE CONFIGURATION BOXES (see install webms for help)
  24.  
  25. /** JSLint excludes */
  26. /*jslint browser: true*/
  27. /*global document, console, GM_addStyle, GM_setValue, GM_getValue, GM_registerMenuCommand, GM_xmlhttpRequest, cloneInto, unsafeWindow*/
  28.  
  29. /* WebStorm JSLint ticked:
  30. - uncapitalized constructors
  31. - missing 'use strict' pragma
  32. - many var statements
  33. */
  34.  
  35. /* Right margin: 160 */
  36.  
  37. // DO NOT EDIT ANYTHING IN THIS SCRIPT DIRECTLY - YOUR REGION SHOULD BE CONFIGURED BY USING THE CONFIGURATION BOXES (see install webms for help)
  38. var regions = [];
  39. var radio = "all";
  40. var lastRegion = ""; //used for back button
  41. var regionVariable = 'regionVariableAPI2';
  42. var radioVariable = 'radioVariableAPI2';
  43. var allPostsOnPage = [];
  44. var postNrs = [];
  45. var postRemoveCounter = 60;
  46. var requestRetryInterval = 5000;
  47. var flegsBaseUrl = 'https://raw.githubusercontent.com/flaghunters/Extra-Flags-for-int-/master/flags/';
  48. // remove comment and change link to add country flag icons into selection menu var countryFlegsBaseUrl = 'https://raw.githubusercontent.com/flagzzzz/Extra-Flags-for-4chan/master/flags/';
  49. var flagListFile = 'flag_list.txt';
  50. var backendBaseUrl = 'https://whatisthisimnotgoodwithcomputers.com/';
  51. var postUrl = 'int/post_flag_api2.php';
  52. var getUrl = 'int/get_flags_api2.php';
  53. var shortId = 'witingwc.ef.';
  54. var regionDivider = "||";
  55.  
  56. /** Setup, preferences */
  57. var setup = {
  58. namespace: 'com.whatisthisimnotgoodwithcomputers.extraflagsforint.',
  59. id: "ExtraFlags-setup",
  60. html: function () {
  61.  
  62. var htmlFixedStart = '<div>Extra Flags for 4chan v2</div><br/>';
  63. var htmlBackButton = '<button name="back">Back</button>';
  64. var htmlNextButton = '<button name="forward">Next</button>';
  65. var htmlBackNextButtons = '<div>' + htmlBackButton + htmlNextButton + '</div>';
  66. var htmlSaveButton = '<div><button name="save" title="Pressing &#34;Save Regions&#34; will set your regions to the ones current displayed below.">' +
  67. 'Save Regions</button></div><br/>';
  68. var htmlHelpText = '<label name="' + shortId + 'label"> You can go as deep as you like, regions stack.<br/>' +
  69. 'For example; United States, California, Los Angeles<br/></label>' +
  70. '<label>Country must match your flag! Your flag not here? Open issue here:<br/>' +
  71. '<a href="https://github.com/flaghunters/Extra-Flags-for-4chan/issues" style="color:blue">' +
  72. 'https://github.com/flaghunters/Extra-Flags-for-4chan/issues</a></label>';
  73. var filterRadio = '<br/><br/><form id="filterRadio">' +
  74. '<input type="radio" name="filterRadio" id="filterRadioall" style="display: inline !important;" value="all"><label>Show country + ALL regions.</label>' +
  75. '<br/><input type="radio" name="filterRadio" id="filterRadiofirst" style="display: inline !important;" value="first"><label>Only show country + FIRST region.</label>' +
  76. '<br/><input type="radio" name="filterRadio" id="filterRadiolast" style="display: inline !important;" value="last"><label>Only show country + LAST region. (v1/old format)</label>' +
  77. '</form>';
  78.  
  79. if (regions.length > 1) {
  80. var selectMenuFlags = "Regional flags selected: ";
  81. var path = flegsBaseUrl + "/" + regions[0];
  82. for (var i = 1; i < regions.length; i++) {
  83. path += "/" + regions[i];
  84. selectMenuFlags += "<img src='" + path + ".png'" + " title='" + regions[i] + "'> ";
  85. }
  86. selectMenuFlags += "<br/>";
  87. return htmlFixedStart + '<div>Region: <br/><select id="' + shortId + 'countrySelect">' +
  88. '</select></div><br/>' + htmlBackNextButtons +
  89. '<br/>' + htmlSaveButton + '</div>' + selectMenuFlags + htmlHelpText + filterRadio;
  90. }
  91.  
  92. if (regions.length == 1) {
  93. var selectMenuFlags = "<br/>";
  94. return htmlFixedStart + '<div>Region: <br/><select id="' + shortId + 'countrySelect">' +
  95. '</select></div><br/>' + htmlBackNextButtons +
  96. '<br/>' + '</div><br/><br/>' + selectMenuFlags + htmlHelpText + filterRadio;
  97. }
  98.  
  99. return htmlFixedStart + '<div>Country: <br/><select id="' + shortId + 'countrySelect">' +
  100. '</select></div><br/>' + htmlBackNextButtons + '<br/>' + htmlHelpText + filterRadio;
  101.  
  102. },
  103. fillHtml: function (path1) {
  104. if (path1 === "") { //normal call
  105. var path = flegsBaseUrl + "/";
  106. var oldPath = path;
  107. if (regions.length > 0) {
  108. for (var i = 0; i < regions.length; i++) {
  109. oldPath = path;
  110. path += regions[i] + "/";
  111. }
  112. }
  113. var pathNoFlagList = path;
  114. } else { // end of folder line call
  115. path = path1;
  116. oldPath = "";
  117. var pathNoFlagList = path;
  118. }
  119.  
  120. /* resolve countries which we support */
  121. GM_xmlhttpRequest({
  122. method: "GET",
  123. url: path + flagListFile,
  124. headers: {
  125. "Content-Type": "application/x-www-form-urlencoded"
  126. },
  127. onload: function (response) {
  128. if (response.status == 404) { // detect if there are no more folders
  129. setup.fillHtml(oldPath);
  130. setup.q('forward').disabled = true; // disable next button
  131. } else {
  132. //hide spam, debug purposes only
  133. //console.log(response.responseText);
  134. var countrySelect = document.getElementById(shortId + 'countrySelect'),
  135. countriesAvailable = response.responseText.split('\n');
  136.  
  137. for (var countriesCounter = 0; countriesCounter < countriesAvailable.length - 1; countriesCounter++) {
  138. var opt = document.createElement('option');
  139. opt.value = countriesAvailable[countriesCounter];
  140.  
  141. if (regions.length > 0) {
  142. opt.innerHTML = countriesAvailable[countriesCounter] + " " + "<img src=\"" + flegsBaseUrl + pathNoFlagList + countriesAvailable[countriesCounter] + ".png\"" + " title=\"" + countriesAvailable[countriesCounter] + "\">";
  143. } else {
  144. opt.innerHTML = countriesAvailable[countriesCounter]; // remove comment to enable country flags in the selection menu + " " + "<img src=\"" + countryFlegsBaseUrl + countriesAvailable[countriesCounter] + ".png\"" + " title=\"" + countriesAvailable[countriesCounter] + "\">";
  145. }
  146.  
  147.  
  148. if (lastRegion != "" && countriesAvailable[countriesCounter] === lastRegion) { // automatically select last selected when going up a folder
  149. opt.selected = "selected";
  150. } else if (oldPath == "" && countriesAvailable[countriesCounter] === regions[regions.length - 1]) { // show final selected when no more
  151. // folders detected
  152. opt.selected = "selected";
  153. }
  154. countrySelect.appendChild(opt);
  155. }
  156. }
  157.  
  158. }
  159. });
  160. },
  161. setRadio: function() {
  162. var radioStatus = setup.load(radioVariable);
  163. if (!radioStatus || radioStatus === "" || radioStatus === "undefined") {
  164. radioStatus = "all";
  165. }
  166. var radioButton = document.getElementById("filterRadio" + radioStatus);
  167. radioButton.checked = true;
  168. },
  169. q: function (n) {
  170. return document.querySelector('#' + this.id + ' *[name="' + n + '"]');
  171. },
  172. removeExtra: function () {
  173. if (regions.length > 0) {
  174. lastRegion = regions[regions.length - 1];
  175. regions.pop();
  176. }
  177. setup.show();
  178. },
  179. show: function () {
  180. /* remove setup window if existing */
  181. var setup_el = document.getElementById(setup.id);
  182. if (setup_el) {
  183. setup_el.parentNode.removeChild(setup_el);
  184. }
  185. /* create new setup window */
  186. GM_addStyle('\
  187. #' + setup.id + ' { position:fixed;z-index:10001;top:40px;right:40px;padding:20px 30px;background-color:white;width:auto;border:1px solid black }\
  188. #' + setup.id + ' * { color:black;text-align:left;line-height:normal;font-size:12px }\
  189. #' + setup.id + ' div { text-align:center;font-weight:bold;font-size:14px }'
  190. );
  191. setup_el = document.createElement('div');
  192. setup_el.id = setup.id;
  193. setup_el.innerHTML = setup.html();
  194. setup.fillHtml("", "");
  195.  
  196. document.body.appendChild(setup_el);
  197.  
  198. setup.setRadio();
  199.  
  200. /* button listeners */
  201. setup.q('back').addEventListener('click', function () {
  202. if (regions.length > 0) {
  203. if (setup.q('forward').disabled == true) {
  204. setup.q('forward').disabled = false; // reenable next button
  205. }
  206. lastRegion = regions[regions.length - 1];
  207. regions.pop();
  208. setup.show();
  209. }
  210. }, false);
  211.  
  212. setup.q('forward').addEventListener('click', function () {
  213. var e = document.getElementById(shortId + "countrySelect");
  214. var temp = e.options[e.selectedIndex].value;
  215. lastRegion = "";
  216. if (temp != "" && regions[regions.length - 1] != temp) {
  217. this.disabled = true;
  218. this.innerHTML = 'Saving...';
  219.  
  220. lastRegion = regions[regions.length - 1];
  221. regions.push(temp);
  222. setup.show();
  223. }
  224.  
  225. }, false);
  226.  
  227. setup.q('save').addEventListener('click', function () {
  228. var e = document.getElementById(shortId + "countrySelect");
  229. var temp = e.options[e.selectedIndex].value;
  230.  
  231. if (regions[regions.length - 1] === "") { //prevent last spot from being blank
  232. regions.pop();
  233. }
  234. lastRegion = "";
  235.  
  236. radio = document.querySelector('input[name="filterRadio"]:checked').value;
  237. setup.save(radioVariable, radio);
  238.  
  239. alert('Flags set: ' + regions + '\n\n' + 'Refresh all your 4chan tabs and be sure to post using the quick reply window!');
  240.  
  241. this.disabled = true;
  242. this.innerHTML = 'Saving...';
  243. setup_el.parentNode.removeChild(setup_el);
  244. setup.save(regionVariable, regions);
  245.  
  246. }, false);
  247. },
  248. save: function (k, v) {
  249. GM_setValue(setup.namespace + k, v);
  250. },
  251. load: function (k) {
  252. return GM_getValue(setup.namespace + k);
  253. },
  254. init: function () {
  255. //GM_registerMenuCommand('Extra Flags setup', setup.show;
  256. GM_registerMenuCommand('Extra Flags setup', setup.show);
  257. }
  258. };
  259.  
  260. /** Prompt to set region if regionVariable is empty */
  261. regions = setup.load(regionVariable);
  262. radio = setup.load(radioVariable);
  263. if (!regions) {
  264. regions = [];
  265. setTimeout(function () {
  266. if (window.confirm("Extra Flags: No region detected, set it up now?") === true) {
  267. setup.show();
  268. }
  269. }, 2000);
  270. }
  271. if (!radio || radio === "" || radio === "undefined") {
  272. radio = "all";
  273. }
  274.  
  275. /** parse the posts already on the page before thread updater kicks in */
  276. function parseOriginalPosts() {
  277. var tempAllPostsOnPage = document.getElementsByClassName('postContainer');
  278. allPostsOnPage = Array.prototype.slice.call(tempAllPostsOnPage); //convert from element list to javascript array
  279. postNrs = allPostsOnPage.map(function (p) {
  280. return p.id.replace("pc", "");
  281. });
  282. }
  283.  
  284. /** the function to get the flags from the db uses postNrs
  285. * member variable might not be very nice but it's the easiest approach here */
  286. function onFlagsLoad(response) {
  287. //exit on error
  288. if (response.status !== 200) {
  289. console.log("Could not fetch flags, status: " + response.status);
  290. console.log(response.statusText);
  291. setTimeout(resolveRefFlags, requestRetryInterval);
  292. return;
  293. }
  294.  
  295. var jsonData = JSON.parse(response.responseText);
  296.  
  297. jsonData.forEach(function (post) {
  298. var postToAddFlagTo = document.getElementById("pc" + post.post_nr),
  299. postInfo = postToAddFlagTo.getElementsByClassName('postInfo')[0],
  300. nameBlock = postInfo.getElementsByClassName('nameBlock')[0],
  301. currentFlag = nameBlock.getElementsByClassName('flag')[0],
  302. postedRegions = post.region.split(regionDivider);
  303.  
  304. if (postedRegions.length > 0 && !(currentFlag === undefined)) {
  305. var path = currentFlag.title;
  306. for (var i = 0; i < postedRegions.length; i++) {
  307. path += "/" + postedRegions[i];
  308.  
  309. // this is probably quite a dirty fix, but it's fast
  310. if ((radio === "all") || (radio === "first" && i === 0) || (radio === "last" && i === (postedRegions.length - 1))) {
  311. var newFlag = document.createElement('a');
  312. nameBlock.appendChild(newFlag);
  313.  
  314. var lastI = i;
  315. if (radio === 'last') {
  316. lastI = 0;
  317. }
  318.  
  319. var newFlagImgOpts = 'onerror="(function () {var extraFlagsImgEl = document.getElementById(\'pc' + post.post_nr +
  320. '\').getElementsByClassName(\'extraFlag\')[' + lastI +
  321. '].firstElementChild; if (!/\\/empty\\.png$/.test(extraFlagsImgEl.src)) {extraFlagsImgEl.src = \'' +
  322. flegsBaseUrl + 'empty.png\';}})();"';
  323.  
  324. newFlag.innerHTML = "<img src=\"" + flegsBaseUrl + path + ".png\"" + newFlagImgOpts + " title=\"" + postedRegions[i] + "\">";
  325. newFlag.className = "extraFlag";
  326.  
  327. if (i > 0) {
  328. newFlag.href = "https://www.google.com/search?q=" + postedRegions[i] + ", " + postedRegions[i - 1];
  329. } else {
  330. newFlag.href = "https://www.google.com/search?q=" + postedRegions[i] + ", " + currentFlag.title;
  331. }
  332.  
  333. newFlag.target = '_blank';
  334. //padding format: TOP x RIGHT_OF x BOTTOM x LEFT_OF
  335. newFlag.style = "padding: 0px 0px 0px 5px; vertical-align:;display: inline-block; width: 16px; height: 11px; position: relative;";
  336.  
  337. console.log("resolved " + postedRegions[i]);
  338. }
  339. }
  340. }
  341.  
  342. //postNrs are resolved and should be removed from this variable
  343. var index = postNrs.indexOf(post.post_nr);
  344. if (index > -1) {
  345. postNrs.splice(index, 1);
  346. }
  347. });
  348.  
  349. //removing posts older than the time limit (they likely won't resolve)
  350. var timestampMinusPostRemoveCounter = Math.round(+new Date() / 1000) - postRemoveCounter;
  351.  
  352. postNrs.forEach(function (post_nr) {
  353. var postToAddFlagTo = document.getElementById("pc" + post_nr),
  354. postInfo = postToAddFlagTo.getElementsByClassName('postInfo')[0],
  355. dateTime = postInfo.getElementsByClassName('dateTime')[0];
  356.  
  357. if (dateTime.getAttribute("data-utc") < timestampMinusPostRemoveCounter) {
  358. var index = postNrs.indexOf(post_nr);
  359. if (index > -1) {
  360. postNrs.splice(index, 1);
  361. }
  362. }
  363. });
  364. }
  365.  
  366. /** fetch flags from db */
  367. function resolveRefFlags() {
  368. var boardID = window.location.pathname.split('/')[1];
  369. if (boardID === "int" || boardID === "sp" || boardID === "pol" || boardID === "bant") {
  370.  
  371. GM_xmlhttpRequest({
  372. method: "POST",
  373. url: backendBaseUrl + getUrl,
  374. data: "post_nrs=" + encodeURIComponent(postNrs) + "&" + "board=" + encodeURIComponent(boardID),
  375. headers: {
  376. "Content-Type": "application/x-www-form-urlencoded"
  377. },
  378. onload: onFlagsLoad
  379. });
  380. }
  381. }
  382.  
  383. /** send flag to system on 4chan x (v2, loadletter, v3 untested) post
  384. * handy comment to save by ccd0
  385. * console.log(e.detail.boardID); // board name (string)
  386. * console.log(e.detail.threadID); // thread number (integer in ccd0, string in loadletter)
  387. * console.log(e.detail.postID); // post number (integer in ccd0, string in loadletter) */
  388. document.addEventListener('QRPostSuccessful', function (e) {
  389. //setTimeout to support greasemonkey 1.x
  390. setTimeout(function () {
  391. GM_xmlhttpRequest({
  392. method: "POST",
  393. url: backendBaseUrl + postUrl,
  394. data: "post_nr=" + encodeURIComponent(e.detail.postID) + "&" + "board=" + encodeURIComponent(e.detail.boardID) + "&" + "regions=" +
  395. encodeURIComponent(regions.slice(1).join(regionDivider)),
  396. headers: {
  397. "Content-Type": "application/x-www-form-urlencoded"
  398. },
  399. onload: function (response) {
  400. //hide spam, debug purposes only
  401. //console.log(response.responseText);
  402. }
  403. });
  404. }, 0);
  405. }, false);
  406.  
  407. /** send flag to system on 4chan inline post */
  408. document.addEventListener('4chanQRPostSuccess', function (e) {
  409. var boardID = window.location.pathname.split('/')[1];
  410. var evDetail = e.detail || e.wrappedJSObject.detail;
  411. //setTimeout to support greasemonkey 1.x
  412. setTimeout(function () {
  413. GM_xmlhttpRequest({
  414. method: "POST",
  415. url: backendBaseUrl + postUrl,
  416. data: "post_nr=" + encodeURIComponent(evDetail.postId) + "&" + "board=" + encodeURIComponent(boardID) + "&" + "regions=" +
  417. encodeURIComponent(regions.slice(1).join(regionDivider)),
  418. headers: {
  419. "Content-Type": "application/x-www-form-urlencoded"
  420. },
  421. onload: function (response) {
  422. //hide spam, debug only
  423. //console.log(response.responseText);
  424. }
  425. });
  426. }, 0);
  427. }, false);
  428.  
  429. /** Listen to post updates from the thread updater for 4chan x v2 (loadletter) and v3 (ccd0 + ?) */
  430. document.addEventListener('ThreadUpdate', function (e) {
  431. var evDetail = e.detail || e.wrappedJSObject.detail;
  432. var evDetailClone = typeof cloneInto === 'function' ? cloneInto(evDetail, unsafeWindow) : evDetail;
  433.  
  434. //ignore if 404 event
  435. if (evDetail[404] === true) {
  436. return;
  437. }
  438.  
  439. setTimeout(function () {
  440. //add to temp posts and the DOM element to allPostsOnPage
  441. evDetailClone.newPosts.forEach(function (post_board_nr) {
  442. var post_nr = post_board_nr.split('.')[1];
  443. postNrs.push(post_nr);
  444. var newPostDomElement = document.getElementById("pc" + post_nr);
  445. allPostsOnPage.push(newPostDomElement);
  446. });
  447.  
  448. }, 0);
  449. //setTimeout to support greasemonkey 1.x
  450. setTimeout(resolveRefFlags, 0);
  451. }, false);
  452.  
  453. /** Listen to post updates from the thread updater for inline extension */
  454. document.addEventListener('4chanThreadUpdated', function (e) {
  455. var evDetail = e.detail || e.wrappedJSObject.detail;
  456.  
  457. var threadID = window.location.pathname.split('/')[3];
  458. var postsContainer = Array.prototype.slice.call(document.getElementById('t' + threadID).childNodes);
  459. var lastPosts = postsContainer.slice(Math.max(postsContainer.length - evDetail.count, 1)); //get the last n elements (where n is evDetail.count)
  460.  
  461. //add to temp posts and the DOM element to allPostsOnPage
  462. lastPosts.forEach(function (post_container) {
  463. var post_nr = post_container.id.replace("pc", "");
  464. postNrs.push(post_nr);
  465. allPostsOnPage.push(post_container);
  466. });
  467. //setTimeout to support greasemonkey 1.x
  468. setTimeout(resolveRefFlags, 0);
  469. }, false);
  470.  
  471. /** START fix flag alignment on chrome */
  472. function addGlobalStyle(css) {
  473. var head, style;
  474. head = document.getElementsByTagName('head')[0];
  475. if (!head) {
  476. return;
  477. }
  478. style = document.createElement('style');
  479. style.type = 'text/css';
  480. style.innerHTML = css;
  481. head.appendChild(style);
  482. }
  483.  
  484. if (navigator.userAgent.toLowerCase().indexOf('webkit') > -1) {
  485. addGlobalStyle('.flag{top: 0px !important;left: -1px !important}');
  486. }
  487. /** END fix flag alignment on chrome */
  488.  
  489. /** setup init and start first calls */
  490. setup.init();
  491. parseOriginalPosts();
  492. resolveRefFlags();