client.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985
  1. /*
  2. Global functions
  3. */
  4. function htmlspecialchars(str){
  5. var map = {
  6. '&': '&',
  7. '<': '&lt;',
  8. '>': '&gt;',
  9. '"': '&quot;',
  10. "'": '&#039;'
  11. }
  12. return str.replace(/[&<>"']/g, function(m){return map[m];});
  13. }
  14. function htmlspecialchars_decode(str){
  15. var map = {
  16. '&amp;': '&',
  17. '&lt;': '<',
  18. '&gt;': '>',
  19. '&quot;': '"',
  20. '&#039;': "'"
  21. }
  22. return str.replace(/&amp;|&lt;|&gt;|&quot;|&#039;/g, function(m){return map[m];});
  23. }
  24. function is_click_within(elem, classname, is_id = false){
  25. while(true){
  26. if(elem === null){
  27. return false;
  28. }
  29. if(
  30. (
  31. is_id === false &&
  32. elem.className == classname
  33. ) ||
  34. (
  35. is_id === true &&
  36. elem.id == classname
  37. )
  38. ){
  39. return elem;
  40. }
  41. elem = elem.parentElement;
  42. }
  43. }
  44. /*
  45. Prevent GET parameter pollution
  46. */
  47. var form = document.getElementsByTagName("form");
  48. if(
  49. form.length !== 0 &&
  50. window.location.pathname != "/" &&
  51. window.location.pathname != "/settings.php" &&
  52. window.location.pathname != "/settings"
  53. ){
  54. form = form[0];
  55. var scraper_dropdown = document.getElementsByName("scraper")[0];
  56. scraper_dropdown.addEventListener("change", function(choice){
  57. submit(form);
  58. });
  59. form.addEventListener("submit", function(e){
  60. e.preventDefault();
  61. submit(e.srcElement);
  62. });
  63. }
  64. function submit(e){
  65. var GET = "";
  66. var first = true;
  67. if((s = document.getElementsByName("s")).length !== 0){
  68. GET += "?s=" + encodeURIComponent(s[0].value).replaceAll("%20", "+");
  69. first = false;
  70. }
  71. Array.from(
  72. e.getElementsByTagName("select")
  73. ).concat(
  74. Array.from(
  75. e.getElementsByTagName("input")
  76. )
  77. ).forEach(function(el){
  78. var firstelem = el.getElementsByTagName("option");
  79. if(
  80. (
  81. (
  82. firstelem.length === 0 ||
  83. firstelem[0].value != el.value
  84. ) &&
  85. el.name != "" &&
  86. el.value != "" &&
  87. el.name != "s"
  88. ) ||
  89. el.name == "scraper" ||
  90. el.name == "nsfw"
  91. ){
  92. if(first){
  93. GET += "?";
  94. first = false;
  95. }else{
  96. GET += "&";
  97. }
  98. GET += encodeURIComponent(el.name).replaceAll("%20", "+") + "=" + encodeURIComponent(el.value).replaceAll("%20", "+");
  99. }
  100. });
  101. window.location.href = GET;
  102. }
  103. /*
  104. Hide show more button when it's not needed on answers
  105. */
  106. var answer_div = document.getElementsByClassName("answer");
  107. if(answer_div.length !== 0){
  108. answer_div = Array.from(answer_div);
  109. var spoiler_button_div = Array.from(document.getElementsByClassName("spoiler-button"));
  110. // execute on pageload
  111. hide_show_more();
  112. window.addEventListener("resize", hide_show_more);
  113. function hide_show_more(){
  114. var height = window.innerWidth >= 1000 ? 600 : 200;
  115. for(i=0; i<answer_div.length; i++){
  116. if(answer_div[i].scrollHeight < height){
  117. spoiler_button_div[i].style.display = "none";
  118. document.getElementById(spoiler_button_div[i].htmlFor).checked = true;
  119. }else{
  120. spoiler_button_div[i].style.display = "block";
  121. }
  122. }
  123. }
  124. }
  125. switch(document.location.pathname){
  126. case "/web":
  127. case "/web.php":
  128. var image_class = "image";
  129. break;
  130. case "/images":
  131. case "/images.php":
  132. var image_class = "thumb";
  133. break;
  134. default:
  135. var image_class = null;
  136. }
  137. if(image_class !== null){
  138. /*
  139. Add popup to document
  140. */
  141. var popup_bg = document.createElement("div");
  142. popup_bg.id = "popup-bg";
  143. document.body.appendChild(popup_bg);
  144. // enable/disable pointer events
  145. if(!document.cookie.includes("bg_noclick=yes")){
  146. popup_bg.style.pointerEvents = "none";
  147. }
  148. var popup_status = document.createElement("div");
  149. popup_status.id = "popup-status";
  150. document.body.appendChild(popup_status);
  151. var popup_body = document.createElement("div");
  152. popup_body.id = "popup";
  153. document.body.appendChild(popup_body);
  154. // import popup
  155. var popup_body = document.getElementById("popup");
  156. var popup_status = document.getElementById("popup-status");
  157. var popup_image = null; // is set later on popup click
  158. // image metadata
  159. var collection = []; // will contain width, height, image URL
  160. var collection_index = 0;
  161. // event handling helper variables
  162. var is_popup_shown = false;
  163. var mouse_down = false;
  164. var mouse_move = false;
  165. var move_x = 0;
  166. var move_y = 0;
  167. var target_is_popup = false;
  168. var mirror_x = false;
  169. var mirror_y = false;
  170. var rotation = 0;
  171. /*
  172. Image dragging (mousedown)
  173. */
  174. document.addEventListener("mousedown", function(div){
  175. if(div.buttons !== 1){
  176. return;
  177. }
  178. mouse_down = true;
  179. mouse_move = false;
  180. if(is_click_within(div.target, "popup", true) === false){
  181. target_is_popup = false;
  182. }else{
  183. target_is_popup = true;
  184. var pos = popup_body.getBoundingClientRect();
  185. move_x = div.x - pos.x;
  186. move_y = div.y - pos.y;
  187. }
  188. });
  189. /*
  190. Image dragging (mousemove)
  191. */
  192. document.addEventListener("mousemove", function(pos){
  193. if(
  194. target_is_popup &&
  195. mouse_down
  196. ){
  197. mouse_move = true;
  198. movepopup(popup_body, pos.clientX - move_x, pos.clientY - move_y);
  199. }
  200. });
  201. /*
  202. Image dragging (mouseup)
  203. */
  204. document.addEventListener("mouseup", function(){
  205. mouse_down = false;
  206. });
  207. /*
  208. Image popup open
  209. */
  210. document.addEventListener("click", function(click){
  211. // should our click trigger image open?
  212. if(
  213. elem = is_click_within(click.target, image_class) ||
  214. click.target.classList.contains("openimg")
  215. ){
  216. event.preventDefault();
  217. is_popup_shown = true;
  218. // reset position params
  219. mirror_x = false;
  220. mirror_y = false;
  221. rotation = 0;
  222. scale = 60;
  223. collection_index = 0;
  224. // get popup data
  225. if(elem === true){
  226. // we clicked a simple image preview
  227. elem = click.target;
  228. var image_url = elem.getAttribute("src");
  229. if(image_url.startsWith("/proxy")){
  230. var match = image_url.match(/i=([^&]+)/);
  231. if(match !== null){
  232. image_url = decodeURIComponent(match[1]);
  233. }
  234. }else{
  235. image_url = htmlspecialchars_decode(image_url);
  236. }
  237. var w = Math.round(click.target.naturalWidth);
  238. var h = Math.round(click.target.naturalHeight);
  239. if(
  240. w === 0 ||
  241. h === 0
  242. ){
  243. w = 100;
  244. h = 100;
  245. }
  246. collection = [
  247. {
  248. "url": image_url,
  249. "width": w,
  250. "height": h
  251. }
  252. ];
  253. var title = "No description provided";
  254. if(click.target.title != ""){
  255. title = click.target.title;
  256. }else{
  257. if(click.target.alt != ""){
  258. title = click.target.alt;
  259. }
  260. }
  261. }else{
  262. if(image_class == "thumb"){
  263. // we're inside image.php
  264. elem =
  265. elem
  266. .parentElement
  267. .parentElement;
  268. var image_url = elem.getElementsByTagName("a")[1].href;
  269. }else{
  270. // we're inside web.php
  271. var image_url = elem.href;
  272. }
  273. collection =
  274. JSON.parse(
  275. elem.getAttribute("data-json")
  276. );
  277. var imagesize = elem.getElementsByTagName("img")[0];
  278. var imagesize_w = 0;
  279. var imagesize_h = 0;
  280. if(imagesize.complete){
  281. imagesize_w = imagesize.naturalWidth;
  282. imagesize_h = imagesize.naturalHeight;
  283. }
  284. if(
  285. imagesize_w === 0 ||
  286. imagesize_h === 0
  287. ){
  288. imagesize_w = 100;
  289. imagesize_h = 100;
  290. }
  291. for(var i=0; i<collection.length; i++){
  292. if(collection[i].width === null){
  293. collection[i].width = imagesize_w;
  294. collection[i].height = imagesize_h;
  295. }
  296. }
  297. var title = elem.title;
  298. }
  299. // prepare HTML
  300. var html =
  301. '<div id="popup-num">(' + collection.length + ')</div>' +
  302. '<div id="popup-dropdown">' +
  303. '<select name="viewer-res" onchange="changeimage(event)">';
  304. for(i=0; i<collection.length; i++){
  305. if(collection[i].url.startsWith("data:")){
  306. var domain = "&lt;Base64 Data&gt;";
  307. }else{
  308. var domain = new URL(collection[i].url).hostname;
  309. }
  310. html += '<option value="' + i + '">' + '(' + collection[i].width + 'x' + collection[i].height + ') ' + domain + '</option>';
  311. }
  312. popup_status.innerHTML =
  313. html + '</select></div>' +
  314. '<a href="' + htmlspecialchars(image_url) + '" rel="noreferrer nofollow "id="popup-title">' + htmlspecialchars(title) + '</a>';
  315. popup_body.innerHTML =
  316. '<img src="' + getproxylink(collection[0].url) + '" draggable="false" id="popup-image">';
  317. // make changes to DOM
  318. popup_body.style.display = "block";
  319. popup_bg.style.display = "block";
  320. popup_status.style.display = "table";
  321. // store for rotation functions & changeimage()
  322. popup_image = document.getElementById("popup-image");
  323. scalepopup(collection[collection_index], scale);
  324. centerpopup();
  325. }else{
  326. // click inside the image viewer
  327. // resize image
  328. if(is_click_within(click.target, "popup", true)){
  329. if(mouse_move === false){
  330. scale = 80;
  331. scalepopup(collection[collection_index], scale);
  332. centerpopup();
  333. }
  334. }else{
  335. if(is_click_within(click.target, "popup-status", true) === false){
  336. // click outside the popup while its open
  337. // close it
  338. if(is_popup_shown){
  339. hidepopup();
  340. }
  341. }
  342. }
  343. }
  344. });
  345. /*
  346. Scale image viewer
  347. */
  348. popup_body.addEventListener("wheel", function(scroll){
  349. event.preventDefault();
  350. if(
  351. scroll.altKey ||
  352. scroll.ctrlKey ||
  353. scroll.shiftKey
  354. ){
  355. var increment = 7;
  356. }else{
  357. var increment = 14;
  358. }
  359. if(scroll.wheelDelta > 0){
  360. // scrolling up
  361. scale = scale + increment;
  362. }else{
  363. // scrolling down
  364. if(scale - increment > 7){
  365. scale = scale - increment;
  366. }
  367. }
  368. // calculate relative size before scroll
  369. var pos = popup_body.getBoundingClientRect();
  370. var x = (scroll.x - pos.x) / pos.width;
  371. var y = (scroll.y - pos.y) / pos.height;
  372. scalepopup(collection[collection_index], scale);
  373. // move popup to % we found
  374. pos = popup_body.getBoundingClientRect();
  375. movepopup(
  376. popup_body,
  377. scroll.clientX - (x * pos.width),
  378. scroll.clientY - (y * pos.height)
  379. );
  380. });
  381. /*
  382. Keyboard controls
  383. */
  384. document.addEventListener("keydown", function(key){
  385. // close popup
  386. if(
  387. is_popup_shown &&
  388. key.keyCode === 27
  389. ){
  390. hidepopup();
  391. return;
  392. }
  393. if(is_popup_shown === false){
  394. return;
  395. }
  396. if(
  397. key.altKey ||
  398. key.ctrlKey ||
  399. key.shiftKey
  400. ){
  401. // mirror image
  402. switch(key.keyCode){
  403. case 37:
  404. // left
  405. key.preventDefault();
  406. mirror_x = true;
  407. break;
  408. case 38:
  409. // up
  410. key.preventDefault();
  411. mirror_y = false;
  412. break;
  413. case 39:
  414. // right
  415. key.preventDefault();
  416. mirror_x = false;
  417. break;
  418. case 40:
  419. // down
  420. key.preventDefault();
  421. mirror_y = true;
  422. break;
  423. }
  424. }else{
  425. // rotate image
  426. switch(key.keyCode){
  427. case 37:
  428. // left
  429. key.preventDefault();
  430. rotation = -90;
  431. break;
  432. case 38:
  433. // up
  434. key.preventDefault();
  435. rotation = 0;
  436. break;
  437. case 39:
  438. // right
  439. key.preventDefault();
  440. rotation = 90;
  441. break;
  442. case 40:
  443. // down
  444. key.preventDefault();
  445. rotation = -180;
  446. break;
  447. }
  448. }
  449. popup_image.style.transform =
  450. "scale(" +
  451. (mirror_x ? "-1" : "1") +
  452. ", " +
  453. (mirror_y ? "-1" : "1") +
  454. ") " +
  455. "rotate(" +
  456. rotation + "deg" +
  457. ")";
  458. });
  459. }
  460. function getproxylink(url){
  461. if(url.startsWith("data:")){
  462. return htmlspecialchars(url);
  463. }else{
  464. return '/proxy?i=' + encodeURIComponent(url);
  465. }
  466. }
  467. function hidepopup(){
  468. is_popup_shown = false;
  469. popup_status.style.display = "none";
  470. popup_body.style.display = "none";
  471. popup_bg.style.display = "none";
  472. }
  473. function scalepopup(size, scale){
  474. var ratio =
  475. Math.min(
  476. (window.innerWidth * (scale / 100)) / collection[collection_index].width, (window.innerHeight * (scale / 100)) / collection[collection_index].height
  477. );
  478. popup_body.style.width = size.width * ratio + "px";
  479. popup_body.style.height = size.height * ratio + "px";
  480. }
  481. function centerpopup(){
  482. var size = popup_body.getBoundingClientRect();
  483. var size = {
  484. "width": parseInt(size.width),
  485. "height": parseInt(size.height)
  486. };
  487. movepopup(
  488. popup_body,
  489. (window.innerWidth / 2) - (size.width / 2),
  490. (window.innerHeight / 2) - (size.height / 2)
  491. );
  492. }
  493. function movepopup(popup_body, x, y){
  494. popup_body.style.left = x + "px";
  495. popup_body.style.top = y + "px";
  496. }
  497. function changeimage(event){
  498. // reset rotation params
  499. mirror_x = false;
  500. mirror_y = false;
  501. rotation = 0;
  502. scale = 60;
  503. collection_index = parseInt(event.target.value);
  504. // we set innerHTML otherwise old image lingers a little
  505. popup_body.innerHTML =
  506. '<img src="' + getproxylink(collection[collection_index].url) + '" draggable="false" id="popup-image">';
  507. // store for rotation functions & changeimage()
  508. popup_image = document.getElementById("popup-image");
  509. scalepopup(collection[collection_index], scale);
  510. centerpopup();
  511. }
  512. var searchbox_wrapper = document.getElementsByClassName("searchbox");
  513. if(searchbox_wrapper.length !== 0){
  514. searchbox_wrapper = searchbox_wrapper[0];
  515. var searchbox = searchbox_wrapper.getElementsByTagName("input")[1];
  516. /*
  517. Textarea shortcuts
  518. */
  519. document.addEventListener("keydown", function(key){
  520. switch(key.keyCode){
  521. case 191:
  522. // 191 = /
  523. if(document.activeElement.tagName == "INPUT"){
  524. // already focused, ignore
  525. break;
  526. }
  527. if(
  528. typeof is_popup_shown != "undefined" &&
  529. is_popup_shown
  530. ){
  531. hidepopup();
  532. }
  533. window.scrollTo(0, 0);
  534. searchbox.focus();
  535. key.preventDefault();
  536. break;
  537. }
  538. });
  539. /*
  540. Autocompleter
  541. */
  542. if( // make sure the user wants it
  543. document.cookie.includes("scraper_ac=") &&
  544. document.cookie.includes("scraper_ac=disabled") === false
  545. ){
  546. var autocomplete_cache = [];
  547. var focuspos = -1;
  548. var list = [];
  549. var autocomplete_div = document.getElementsByClassName("autocomplete")[0];
  550. if(
  551. document.cookie.includes("scraper_ac=auto") &&
  552. typeof scraper_dropdown != "undefined"
  553. ){
  554. var ac_req_appendix = "&scraper=" + scraper_dropdown.value;
  555. }else{
  556. var ac_req_appendix = "";
  557. }
  558. function getsearchboxtext(){
  559. var value =
  560. searchbox.value
  561. .trim()
  562. .replace(
  563. / +/g,
  564. " "
  565. )
  566. .toLowerCase();
  567. return value;
  568. }
  569. searchbox.addEventListener("input", async function(){
  570. // ratelimit on input only
  571. // dont ratelimit if we already have res
  572. if(typeof autocomplete_cache[getsearchboxtext()] != "undefined"){
  573. await getac();
  574. }else{
  575. await getac_ratelimit();
  576. }
  577. });
  578. async function getac(){
  579. var curvalue = getsearchboxtext();
  580. if(curvalue == ""){
  581. // hide autocompleter
  582. autocomplete_div.style.display = "none";
  583. return;
  584. }
  585. if(typeof autocomplete_cache[curvalue] == "undefined"){
  586. /*
  587. Fetch autocomplete
  588. */
  589. // make sure we dont fetch same thing twice
  590. autocomplete_cache[curvalue] = [];
  591. var res = await fetch("/api/v1/ac?s=" + (encodeURIComponent(curvalue).replaceAll("%20", "+")) + ac_req_appendix);
  592. if(!res.ok){
  593. return;
  594. }
  595. var json = await res.json();
  596. autocomplete_cache[curvalue] = json[1];
  597. if(curvalue == getsearchboxtext()){
  598. render_ac(curvalue, autocomplete_cache[curvalue]);
  599. }
  600. return;
  601. }
  602. render_ac(curvalue, autocomplete_cache[curvalue]);
  603. }
  604. var ac_func = null;
  605. function getac_ratelimit(){
  606. return new Promise(async function(resolve, reject){
  607. if(ac_func !== null){
  608. clearTimeout(ac_func);
  609. }//else{
  610. // no ratelimits
  611. //getac();
  612. //}
  613. ac_func =
  614. setTimeout(function(){
  615. ac_func = null;
  616. getac(); // get results after 100ms of no keystroke
  617. resolve();
  618. }, 200);
  619. });
  620. }
  621. function render_ac(query, list){
  622. if(list.length === 0){
  623. autocomplete_div.style.display = "none";
  624. return;
  625. }
  626. html = "";
  627. // prepare regex
  628. var highlight = query.split(" ");
  629. var regex = [];
  630. for(var k=0; k<highlight.length; k++){
  631. // espace regex
  632. regex.push(
  633. highlight[k].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
  634. );
  635. }
  636. regex = new RegExp(highlight.join("|"), "gi");
  637. for(var i=0; i<list.length; i++){
  638. html +=
  639. '<div tabindex="0" class="entry" onclick="handle_entry_click(this);">' +
  640. htmlspecialchars(
  641. list[i]
  642. ).replace(
  643. regex,
  644. '<u>$&</u>'
  645. ) +
  646. '</div>';
  647. }
  648. autocomplete_div.innerHTML = html;
  649. autocomplete_div.style.display = "block";
  650. }
  651. var should_focus = false;
  652. document.addEventListener("keydown", function(event){
  653. if(event.key == "Escape"){
  654. document.activeElement.blur();
  655. focuspos = -1;
  656. autocomplete_div.style.display = "none";
  657. return;
  658. }
  659. if(
  660. is_click_within(event.target, "searchbox") === false ||
  661. typeof autocomplete_cache[getsearchboxtext()] == "undefined"
  662. ){
  663. return;
  664. }
  665. switch(event.key){
  666. case "ArrowUp":
  667. event.preventDefault();
  668. focuspos--;
  669. if(focuspos === -2){
  670. focuspos = autocomplete_cache[getsearchboxtext()].length - 1;
  671. }
  672. break;
  673. case "ArrowDown":
  674. case "Tab":
  675. event.preventDefault();
  676. focuspos++;
  677. if(focuspos >= autocomplete_cache[getsearchboxtext()].length){
  678. focuspos = -1;
  679. }
  680. break;
  681. case "Enter":
  682. should_focus = true;
  683. if(focuspos !== -1){
  684. // replace input content
  685. event.preventDefault();
  686. searchbox.value =
  687. autocomplete_div.getElementsByClassName("entry")[focuspos].innerText;
  688. break;
  689. }
  690. break;
  691. default:
  692. focuspos = -1;
  693. break;
  694. }
  695. if(focuspos === -1){
  696. searchbox.focus();
  697. return;
  698. }
  699. autocomplete_div.getElementsByClassName("entry")[focuspos].focus();
  700. });
  701. window.addEventListener("blur", function(){
  702. autocomplete_div.style.display = "none";
  703. });
  704. document.addEventListener("keyup", function(event){
  705. // handle ENTER key on entry
  706. if(should_focus){
  707. should_focus = false;
  708. searchbox.focus();
  709. }
  710. });
  711. document.addEventListener("mousedown", function(event){
  712. // hide input if click is outside
  713. if(is_click_within(event.target, "searchbox") === false){
  714. autocomplete_div.style.display = "none";
  715. return;
  716. }
  717. });
  718. function handle_entry_click(event){
  719. searchbox.value = event.innerText;
  720. focuspos = -1;
  721. searchbox.focus();
  722. }
  723. searchbox.addEventListener("focus", function(){
  724. focuspos = -1;
  725. getac();
  726. });
  727. }
  728. }