4chan Image Viewer

Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey

  1. // ==UserScript==
  2. // @name 4chan Image Viewer
  3. // @namespace IdontKnowWhatToDoWithThis
  4. // @description Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey
  5. // @match *://*.4chan.org/*/res/*
  6. // @match *://*.4chan.org/*/thread/*
  7. // @match *://*.4channel.org/*/thread/*
  8. // @version 8.4
  9. // @copyright 2019+, Nicholas Perkins
  10. // @source https://github.com/nicholas-s-perkins/4chanImageViewer
  11. // ==/UserScript==
  12. "use strict";
  13. var Viewer;
  14. (function (Viewer) {
  15. /**
  16. * Didn't want to use any external libraries. This is my handy library for dealing with the DOM
  17. */
  18. var DomUtil = /** @class */ (function () {
  19. function DomUtil(obj) {
  20. this._elements = [];
  21. this._listeners = [];
  22. if (obj) {
  23. if (obj instanceof NodeList) {
  24. for (var i = 0; i < obj.length; ++i) {
  25. this._elements.push(obj[i]);
  26. }
  27. }
  28. else {
  29. this._elements.push(obj);
  30. }
  31. }
  32. }
  33. Object.defineProperty(DomUtil.prototype, "elementList", {
  34. get: function () {
  35. return this._elements;
  36. },
  37. enumerable: true,
  38. configurable: true
  39. });
  40. DomUtil.prototype.concat = function (collection) {
  41. if (collection instanceof DomUtil) {
  42. this._elements = this._elements.concat(collection._elements);
  43. }
  44. else {
  45. this._elements = this._elements.concat(DomUtil.formatNodeList(collection));
  46. }
  47. return this;
  48. };
  49. /** Adds a click handler */
  50. DomUtil.prototype.on = function (handler, func) {
  51. var _this = this;
  52. var handlers = handler.split(' ');
  53. this.each(function (element) {
  54. for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) {
  55. var handler_1 = handlers_1[_i];
  56. _this._listeners.push(new Listener(element, handler_1, func));
  57. element.addEventListener(handler_1, func, false);
  58. }
  59. });
  60. return this;
  61. };
  62. DomUtil.prototype.appendTo = function (obj) {
  63. if (typeof obj === 'string') {
  64. DomUtil.get(obj).append(this);
  65. }
  66. else if (obj instanceof DomUtil) {
  67. obj.append(this);
  68. }
  69. else {
  70. new DomUtil(obj).append(this);
  71. }
  72. return this;
  73. };
  74. DomUtil.prototype.off = function (handlerType) {
  75. var remaining = [];
  76. for (var _i = 0, _a = this._listeners; _i < _a.length; _i++) {
  77. var listener = _a[_i];
  78. if (handlerType == null || listener.type === handlerType) {
  79. listener.element.removeEventListener(listener.type, listener.func);
  80. }
  81. else {
  82. remaining.push(listener);
  83. }
  84. }
  85. this._listeners = remaining;
  86. return this;
  87. };
  88. DomUtil.prototype.remove = function () {
  89. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  90. var element = _a[_i];
  91. if (element.parentElement) {
  92. element.parentElement.removeChild(element);
  93. }
  94. }
  95. return this;
  96. };
  97. DomUtil.prototype.prepend = function (obj) {
  98. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  99. var thisElement = _a[_i];
  100. for (var _b = 0, _c = obj._elements; _b < _c.length; _b++) {
  101. var objElement = _c[_b];
  102. if (thisElement.parentElement) {
  103. thisElement.parentElement.insertBefore(objElement, thisElement);
  104. }
  105. }
  106. }
  107. return this;
  108. };
  109. DomUtil.prototype.append = function (obj) {
  110. if (typeof obj === 'string') {
  111. this.each(function (element) {
  112. element.insertAdjacentHTML('beforeend', obj);
  113. });
  114. }
  115. else if (obj instanceof DomUtil) {
  116. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  117. var element = _a[_i];
  118. for (var _b = 0, _c = obj._elements; _b < _c.length; _b++) {
  119. var objEle = _c[_b];
  120. element.appendChild(objEle);
  121. }
  122. }
  123. }
  124. else {
  125. for (var _d = 0, _e = this._elements; _d < _e.length; _d++) {
  126. var element = _e[_d];
  127. element.appendChild(obj);
  128. }
  129. }
  130. return this;
  131. };
  132. DomUtil.prototype.empty = function () {
  133. this.each(function (element) {
  134. while (element.firstChild) {
  135. element.removeChild(element.firstChild);
  136. }
  137. });
  138. return this;
  139. };
  140. DomUtil.prototype.scrollToTop = function () {
  141. if (this._elements.length > 0) {
  142. this._elements[0].scrollTop = 0;
  143. }
  144. return this;
  145. };
  146. DomUtil.prototype.focus = function () {
  147. if (this._elements.length > 0) {
  148. this._elements[0].focus();
  149. }
  150. return this;
  151. };
  152. Object.defineProperty(DomUtil.prototype, "tabIndex", {
  153. set: function (index) {
  154. if (this._elements.length > 0) {
  155. this._elements[0].tabIndex = index;
  156. }
  157. },
  158. enumerable: true,
  159. configurable: true
  160. });
  161. DomUtil.prototype.setAttr = function (attr, value) {
  162. this.each(function (element) {
  163. element[attr] = value;
  164. });
  165. return this;
  166. };
  167. DomUtil.prototype.setText = function (text) {
  168. this.each(function (element) { return element.innerText = "" + text; });
  169. return this;
  170. };
  171. DomUtil.prototype.setStyle = function (styleConfig) {
  172. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  173. var element = _a[_i];
  174. for (var propName in styleConfig) {
  175. // @ts-ignore
  176. element.style[propName] = styleConfig[propName];
  177. }
  178. }
  179. return this;
  180. };
  181. DomUtil.prototype.setData = function (data) {
  182. var _loop_1 = function (element) {
  183. Object.keys(data).forEach(function (propName) {
  184. element.dataset[propName] = data[propName];
  185. });
  186. };
  187. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  188. var element = _a[_i];
  189. _loop_1(element);
  190. }
  191. return this;
  192. };
  193. DomUtil.prototype.replaceWith = function (replacement) {
  194. var replaceEle = replacement._elements;
  195. this.each(function (element) {
  196. if (element.parentElement) {
  197. for (var i = replaceEle.length - 1; i >= 0; i--) {
  198. element.parentElement.insertBefore(replaceEle[i], element);
  199. }
  200. element.parentElement.removeChild(element);
  201. }
  202. });
  203. return this;
  204. };
  205. DomUtil.prototype.html = function (html) {
  206. if (typeof html === 'string') {
  207. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  208. var element = _a[_i];
  209. element.innerHTML = html;
  210. }
  211. }
  212. else {
  213. this.each(function (element) {
  214. DomUtil.get(element).remove();
  215. });
  216. this.append(html);
  217. }
  218. return this;
  219. };
  220. Object.defineProperty(DomUtil.prototype, "length", {
  221. get: function () {
  222. return this._elements.length;
  223. },
  224. enumerable: true,
  225. configurable: true
  226. });
  227. Object.defineProperty(DomUtil.prototype, "id", {
  228. get: function () {
  229. return this._elements.length > 0 ? this._elements[0].id : null;
  230. },
  231. enumerable: true,
  232. configurable: true
  233. });
  234. Object.defineProperty(DomUtil.prototype, "clientHeight", {
  235. get: function () {
  236. return this._elements.length > 0 ? this._elements[0].clientHeight : 0;
  237. },
  238. enumerable: true,
  239. configurable: true
  240. });
  241. Object.defineProperty(DomUtil.prototype, "clientWidth", {
  242. get: function () {
  243. return this._elements.length > 0 ? this._elements[0].clientWidth : 0;
  244. },
  245. enumerable: true,
  246. configurable: true
  247. });
  248. Object.defineProperty(DomUtil.prototype, "offsetHeight", {
  249. get: function () {
  250. return this._elements.length > 0 ? this._elements[0].offsetHeight : 0;
  251. },
  252. enumerable: true,
  253. configurable: true
  254. });
  255. Object.defineProperty(DomUtil.prototype, "offsetWidth", {
  256. get: function () {
  257. return this._elements.length > 0 ? this._elements[0].offsetWidth : 0;
  258. },
  259. enumerable: true,
  260. configurable: true
  261. });
  262. Object.defineProperty(DomUtil.prototype, "tagName", {
  263. get: function () {
  264. return this._elements.length > 0 ? this._elements[0].tagName : null;
  265. },
  266. enumerable: true,
  267. configurable: true
  268. });
  269. DomUtil.prototype.hasClass = function (className) {
  270. return this._elements.length > 0 ? this._elements[0].classList.contains(className) : false;
  271. };
  272. DomUtil.prototype.getAttr = function (attr) {
  273. if (this._elements.length > 0) {
  274. var ele = this._elements[0];
  275. return ele[attr];
  276. }
  277. else {
  278. return null;
  279. }
  280. };
  281. DomUtil.prototype.lightClone = function () {
  282. var newCollection = new DomUtil();
  283. this.each(function (element) {
  284. var newEle = document.createElement(element.tagName);
  285. newEle.className = element.className;
  286. newEle.innerHTML = element.innerHTML;
  287. newCollection._elements.push(newEle);
  288. });
  289. return newCollection;
  290. };
  291. DomUtil.prototype.addClass = function () {
  292. var classNames = [];
  293. for (var _i = 0; _i < arguments.length; _i++) {
  294. classNames[_i] = arguments[_i];
  295. }
  296. this.each(function (element) {
  297. element.classList.add.apply(element.classList, classNames);
  298. });
  299. return this;
  300. };
  301. DomUtil.prototype.removeClass = function () {
  302. var classNames = [];
  303. for (var _i = 0; _i < arguments.length; _i++) {
  304. classNames[_i] = arguments[_i];
  305. }
  306. this.each(function (element) {
  307. element.classList.remove.apply(element.classList, classNames);
  308. });
  309. return this;
  310. };
  311. DomUtil.prototype.each = function (func) {
  312. for (var i = 0; i < this._elements.length; ++i) {
  313. func(this._elements[i], i);
  314. }
  315. return this;
  316. };
  317. /** Finds all sub-elements matching the queryString */
  318. DomUtil.prototype.find = function (queryString) {
  319. var collection = new DomUtil();
  320. for (var _i = 0, _a = this._elements; _i < _a.length; _i++) {
  321. var element = _a[_i];
  322. collection.concat(element.querySelectorAll(queryString));
  323. }
  324. return collection;
  325. };
  326. Object.defineProperty(DomUtil.prototype, "exists", {
  327. get: function () {
  328. return this._elements.length > 0;
  329. },
  330. enumerable: true,
  331. configurable: true
  332. });
  333. /** because screw node lists */
  334. DomUtil.formatNodeList = function (nodes) {
  335. var arr = [];
  336. for (var i = 0; i < nodes.length; ++i) {
  337. arr.push(nodes[i]);
  338. }
  339. return arr;
  340. };
  341. DomUtil.get = function (query) {
  342. if (typeof query === 'string') {
  343. switch (query) {
  344. case 'body':
  345. return new DomUtil(document.body);
  346. case 'head':
  347. return new DomUtil(document.head);
  348. default:
  349. var nodes = document.querySelectorAll(query);
  350. return new DomUtil(nodes);
  351. }
  352. }
  353. else {
  354. return new DomUtil(query);
  355. }
  356. };
  357. DomUtil.getById = function (id) {
  358. var ele = document.getElementById(id);
  359. return new DomUtil(ele);
  360. };
  361. DomUtil.createElement = function (tagName, props) {
  362. var newEle = document.createElement(tagName);
  363. if (props) {
  364. Object.keys(props).forEach(function (propName) {
  365. if (propName == "style") {
  366. newEle.style.cssText = props.style.cssText;
  367. }
  368. else {
  369. newEle[propName] = props[propName];
  370. }
  371. });
  372. }
  373. return new DomUtil(newEle);
  374. };
  375. return DomUtil;
  376. }());
  377. Viewer.DomUtil = DomUtil;
  378. var Listener = /** @class */ (function () {
  379. function Listener(element, type, func) {
  380. this.type = type;
  381. this.func = func;
  382. this.element = element;
  383. }
  384. return Listener;
  385. }());
  386. Viewer.Listener = Listener;
  387. })(Viewer || (Viewer = {}));
  388. var Viewer;
  389. (function (Viewer) {
  390. //IDs for important elements
  391. Viewer.VIEW_ID = "mainView";
  392. Viewer.IMG_ID = "mainImg";
  393. Viewer.CENTER_BOX_ID = "imageBox";
  394. Viewer.TOP_LAYER_ID = "viewerTopLayer";
  395. Viewer.IMG_WRAPPER_ID = 'mainImgWrapper';
  396. Viewer.TEXT_WRAPPER_ID = 'viewerTextWrapper';
  397. Viewer.STYLE_ID = 'viewerStyle';
  398. Viewer.MENU_ID = 'viewerBottomMenu';
  399. Viewer.LEFT_ARROW = 'previousImageButton';
  400. Viewer.RIGHT_ARROW = 'nextImageButton';
  401. Viewer.TOP_MENU_ID = 'viewerMenuHeader';
  402. Viewer.VIEWER_PAGE_DISPLAY = "viewerPageDisplay";
  403. Viewer.VIEWER_TOTAL_DISPLAY = "viewerTotalDisplay";
  404. Viewer.VIEWER_IMG_NAME_DISPLAY = "viewerNameDisplay";
  405. Viewer.STYLE_TEXT = "\n div.reply.highlight,div.reply.highlight-anti{z-index:100 !important;position:fixed !important; top:1%;left:1%;}\n body{overflow:hidden !important;}\n #quote-preview{z-index:100;}\n a.quotelink, div.viewerBacklinks a.quotelink{color:#5c5cff !important;}\n a.quotelink:hover, div.viewerBacklinks a:hover{color:red !important;}\n #" + Viewer.IMG_ID + "{display:block !important; margin:auto;max-width:100%;height:auto;-webkit-user-select: none;cursor:pointer;}\n #" + Viewer.VIEW_ID + "{\n background-color:rgba(0,0,0,0.9);\n z-index:10;\n position:fixed;\n top:0;left:0;bottom:0;right:0;\n overflow:auto;\n text-align:center;\n -webkit-user-select: none;\n }\n #" + Viewer.CENTER_BOX_ID + " {display:flex;align-items:center;justify-content:center;flex-direction: column;min-height:100%;}\n #" + Viewer.IMG_WRAPPER_ID + " {width:100%;}\n #" + Viewer.TOP_LAYER_ID + "{position:fixed;top:0;bottom:0;left:0;right:0;z-index:20;opacity:0;visibility:hidden;transition:all .25s ease;}\n .viewerBlockQuote{color:white;}\n #" + Viewer.TEXT_WRAPPER_ID + "{max-width:60em;display:inline-block; color:gray;-webkit-user-select: all;}\n .bottomMenuShow{visibility:visible;}\n #" + Viewer.MENU_ID + "{box-shadow: -1px -1px 5px #888888;font-size:20px;padding:5px;background-color:white;position:fixed;bottom:0;right:0;z-index:200;}\n #" + Viewer.TOP_MENU_ID + "{font-size:20px;padding:5px;background-color:white;position:fixed;top:0;left:0;text-align:center;width:100%;color:black;z-index:200;}\n .hideCursor{cursor:none !important;}\n .hidden{visibility:hidden}\n .displayNone{display:none;}\n .pagingButtons{font-size:100px;color:white;text-shadow: 1px 1px 10px #27E3EB;z-index: 11;top: 50%;position: fixed;margin-top: -57px;width:100px;cursor:pointer;-webkit-user-select: none;}\n .pagingButtons:hover{color:#27E3EB;text-shadow: 1px 1px 10px #000}\n #" + Viewer.LEFT_ARROW + "{left:0;text-align:left;}\n #" + Viewer.RIGHT_ARROW + "{right:0;text-align:right;}\n @-webkit-keyframes flashAnimation{0%{ text-shadow: none;}100%{text-shadow: 0px 0px 5px blue;}}\n .flash{-webkit-animation: flashAnimation 1s alternate infinite linear;cursor:pointer;}\n .disableClick, .disableClick a{pointer-events: none;}\n ";
  406. })(Viewer || (Viewer = {}));
  407. var Viewer;
  408. (function (Viewer) {
  409. //cookieInfo
  410. var INDEX_KEY = "imageBrowserIndexCookie";
  411. var THREAD_KEY = "imageBrowserThreadCookie";
  412. var WIDTH_KEY = "imageBrowserWidthCookie";
  413. var HEIGHT_KEY = "imageBrowserHeightCookie";
  414. //keycode object. Better than remembering what each code does.
  415. var KEYS = { 38: 'up', 40: 'down', 37: 'left', 39: 'right', 27: 'esc', 86: 'v' };
  416. var BODY = Viewer.DomUtil.get(document.body);
  417. var WINDOW = Viewer.DomUtil.get(window);
  418. var UNSAFE_WINDOW = Viewer.DomUtil.get(typeof unsafeWindow === 'undefined' ? window : unsafeWindow);
  419. var MainView = /** @class */ (function () {
  420. function MainView(imagePostIndex) {
  421. var _this = this;
  422. this.postData = [];
  423. this.linkIndex = 0;
  424. /** Determines if pre-loading can happen*/
  425. this.canPreload = false;
  426. /** determines if height of the image should be fit */
  427. this.shouldFitHeight = false;
  428. this.lastMousePos = { x: 0, y: 0 };
  429. console.log("Building 4chan Image Viewer");
  430. var currentThreadId = Viewer.DomUtil.get('.thread').id;
  431. if (imagePostIndex != undefined) {
  432. this.linkIndex = imagePostIndex;
  433. MainView.setPersistentValue(INDEX_KEY, imagePostIndex);
  434. }
  435. //check if its the last thread opened, if so, remember where the index was.
  436. else if (MainView.getPersistentValue(THREAD_KEY) === currentThreadId) {
  437. var savedVal = MainView.getPersistentValue(INDEX_KEY);
  438. if (savedVal != undefined) {
  439. this.linkIndex = parseInt(savedVal);
  440. }
  441. else {
  442. this.linkIndex = 0;
  443. }
  444. }
  445. else {
  446. this.linkIndex = 0;
  447. MainView.setPersistentValue(INDEX_KEY, 0);
  448. }
  449. //set thread id
  450. MainView.setPersistentValue(THREAD_KEY, currentThreadId);
  451. //Create postData based on 4chan posts
  452. this.postData = Viewer.PostData.getImagePosts(true);
  453. if (this.linkIndex > (this.postData.length - 1)) {
  454. alert('Last saved image index is too large, a thread may have been deleted. Index will be reset. ');
  455. this.linkIndex = 0;
  456. MainView.setPersistentValue(INDEX_KEY, 0);
  457. }
  458. //set shouldFit Height so image can know about it if it loads before menuInit()
  459. var isHeight = MainView.getPersistentValue(HEIGHT_KEY);
  460. this.shouldFitHeight = isHeight ? true : false;
  461. var menuHtml = "\n <label><input id=\"" + WIDTH_KEY + "\" type=\"checkbox\" checked=\"checked\" />Fit Image to Width</label>\n <span>|</span>\n <label><input id=\"" + HEIGHT_KEY + "\" type=\"checkbox\" />Fit Image to Height</label>\n ";
  462. var viewFrag = "\n <style id=\"" + Viewer.STYLE_ID + "\">" + Viewer.STYLE_TEXT + "</style>\n <div id=\"" + Viewer.TOP_MENU_ID + "\" class=\"hidden\">\n <div><span id=\"" + Viewer.VIEWER_PAGE_DISPLAY + "\"></span><span> of </span><span id=\"" + Viewer.VIEWER_TOTAL_DISPLAY + "\"></span></div>\n <div><span id=\"" + Viewer.VIEWER_IMG_NAME_DISPLAY + "\"></span></div>\n </div>\n <div id=\"" + Viewer.VIEW_ID + "\">\n <div id=\"" + Viewer.CENTER_BOX_ID + "\">\n <div id=\"" + Viewer.IMG_WRAPPER_ID + "\">\n <img id=\"" + Viewer.IMG_ID + "\" class=\"hideCursor\"/>\n </div>\n <div id=\"" + Viewer.TEXT_WRAPPER_ID + "\"></div>\n </div>\n <div id=\"" + Viewer.LEFT_ARROW + "\" class=\"pagingButtons hidden\"><span>&#9001;</span></div>\n <div id=\"" + Viewer.RIGHT_ARROW + "\" class=\"pagingButtons hidden\"><span>&#9002;</span></div>\n </div>\n <div id=\"" + Viewer.TOP_LAYER_ID + "\">&nbsp;</div>\n <form id=\"" + Viewer.MENU_ID + "\" class=\"hidden\">" + menuHtml + "</form>\n ";
  463. BODY.append(viewFrag);
  464. this.mainView = Viewer.DomUtil.getById(Viewer.VIEW_ID);
  465. this.centerBox = Viewer.DomUtil.getById(Viewer.CENTER_BOX_ID);
  466. this.mainImg = Viewer.DomUtil.getById(Viewer.IMG_ID);
  467. this.textWrapper = Viewer.DomUtil.getById(Viewer.TEXT_WRAPPER_ID);
  468. this.topLayer = Viewer.DomUtil.getById(Viewer.TOP_LAYER_ID);
  469. this.customStyle = Viewer.DomUtil.getById(Viewer.STYLE_ID);
  470. this.bottomMenu = Viewer.DomUtil.getById(Viewer.MENU_ID);
  471. this.leftArrow = Viewer.DomUtil.getById(Viewer.LEFT_ARROW);
  472. this.rightArrow = Viewer.DomUtil.getById(Viewer.RIGHT_ARROW);
  473. this.topMenu = Viewer.DomUtil.getById(Viewer.TOP_MENU_ID);
  474. this.pageDisplay = Viewer.DomUtil.getById(Viewer.VIEWER_PAGE_DISPLAY);
  475. this.totalDisplay = Viewer.DomUtil.getById(Viewer.VIEWER_TOTAL_DISPLAY);
  476. this.nameDisplay = Viewer.DomUtil.getById(Viewer.VIEWER_IMG_NAME_DISPLAY);
  477. //add handlers
  478. this.centerBox.on('click', function () {
  479. _this.confirmExit();
  480. });
  481. this.textWrapper.on('click', function (event) {
  482. _this.eventStopper(event);
  483. });
  484. this.bottomMenu.on('click', function () {
  485. _this.menuClickHandler();
  486. });
  487. this.leftArrow.on('click', function (event) {
  488. event.stopImmediatePropagation();
  489. _this.previousImg();
  490. });
  491. this.rightArrow.on('click', function (event) {
  492. event.stopImmediatePropagation();
  493. _this.nextImg();
  494. });
  495. //build first image/video tag
  496. this.changeData(0);
  497. //initialize menu
  498. this.menuInit();
  499. //start preloading to next image index
  500. this.canPreload = true;
  501. window.setTimeout(function () {
  502. _this.runImagePreloading(_this.linkIndex);
  503. }, 100);
  504. //some fixes for weird browser behaviors
  505. this.centerBox.setStyle({ outline: '0' });
  506. this.centerBox.tabIndex = 1;
  507. this.centerBox.focus();
  508. //add keybinding listener, unsafeWindow is used here instead because at least in Tampermonkey
  509. //the safe window can fail to remove event listeners.
  510. UNSAFE_WINDOW
  511. .on('keydown', function (event) {
  512. _this.arrowKeyListener(event);
  513. })
  514. .on('mousemove', function (event) {
  515. _this.menuWatcher(event);
  516. });
  517. }
  518. MainView.prototype.menuInit = function () {
  519. var _this = this;
  520. var menuControls = this.bottomMenu.find('input');
  521. menuControls.each(function (input) {
  522. var typedInput = input;
  523. var cookieValue = MainView.getPersistentValue(input.id);
  524. if (cookieValue === 'true') {
  525. typedInput.checked = true;
  526. }
  527. else if (cookieValue === 'false') {
  528. typedInput.checked = false;
  529. }
  530. typedInput.parentElement.classList.toggle('flash', typedInput.checked);
  531. switch (typedInput.id) {
  532. case WIDTH_KEY:
  533. _this.setFitToScreenWidth(typedInput.checked);
  534. break;
  535. case HEIGHT_KEY:
  536. _this.setFitToScreenHeight(typedInput.checked);
  537. break;
  538. }
  539. });
  540. };
  541. MainView.prototype.menuClickHandler = function () {
  542. var _this = this;
  543. var menuControls = this.bottomMenu.find('input');
  544. menuControls.each(function (ele) {
  545. var input = ele;
  546. switch (input.id) {
  547. case WIDTH_KEY:
  548. _this.setFitToScreenWidth(input.checked);
  549. break;
  550. case HEIGHT_KEY:
  551. _this.setFitToScreenHeight(input.checked);
  552. break;
  553. }
  554. input.parentElement.classList.toggle('flash', input.checked);
  555. MainView.setPersistentValue(input.id, input.checked);
  556. });
  557. };
  558. MainView.prototype.windowClick = function (event) {
  559. if (!this) {
  560. return;
  561. }
  562. event.preventDefault();
  563. event.stopImmediatePropagation();
  564. this.nextImg();
  565. };
  566. /* Event function for determining behavior of viewer keypresses */
  567. MainView.prototype.arrowKeyListener = function (event) {
  568. switch (KEYS[event.keyCode]) {
  569. case 'right':
  570. this.nextImg();
  571. break;
  572. case 'left':
  573. this.previousImg();
  574. break;
  575. case 'esc':
  576. this.destroy();
  577. break;
  578. }
  579. };
  580. /* preloads images starting with the index provided */
  581. MainView.prototype.runImagePreloading = function (index) {
  582. var _this = this;
  583. if (this && index < this.postData.length) {
  584. if (this.canPreload) {
  585. //console.log('preloading: ' + index +' of '+(this.postData.length - 1) +' | '+ this.postData[index].imgSrc);
  586. var loadFunc = function () {
  587. _this.runImagePreloading(index + 1);
  588. };
  589. //have yet to figure out how to properly preload video, skip for now
  590. if (this.postData[index].tagType === Viewer.TagType.VIDEO) {
  591. window.setTimeout(loadFunc, 1);
  592. }
  593. else {
  594. var newImage = document.createElement(this.postData[index].tagTypeName);
  595. switch (this.postData[index].tagType) {
  596. case Viewer.TagType.VIDEO:
  597. newImage.oncanplaythrough = loadFunc;
  598. break;
  599. case Viewer.TagType.IMG:
  600. newImage.onload = loadFunc;
  601. break;
  602. }
  603. newImage.onerror = function () {
  604. console.log("imageError");
  605. _this.runImagePreloading(index + 1);
  606. };
  607. newImage.src = this.postData[index].imgSrc;
  608. }
  609. }
  610. }
  611. };
  612. /* Sets the img and message to the next one in the list*/
  613. MainView.prototype.nextImg = function () {
  614. var _this = this;
  615. if (this.linkIndex === this.postData.length - 1) {
  616. this.topLayer.setStyle({
  617. background: 'linear-gradient(to right,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)',
  618. opacity: '.5',
  619. visibility: 'visible'
  620. });
  621. window.setTimeout(function () {
  622. _this.topLayer.setStyle({
  623. opacity: '0',
  624. visibility: 'hidden'
  625. });
  626. }, 500);
  627. }
  628. else {
  629. this.changeData(1);
  630. }
  631. };
  632. /* Sets the img and message to the previous one in the list*/
  633. MainView.prototype.previousImg = function () {
  634. var _this = this;
  635. if (this.linkIndex === 0) {
  636. this.topLayer.setStyle({
  637. background: 'linear-gradient(to left,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)',
  638. opacity: '.5',
  639. visibility: 'visible'
  640. });
  641. window.setTimeout(function () {
  642. _this.topLayer.setStyle({ opacity: '0' });
  643. window.setTimeout(function () {
  644. _this.topLayer.setStyle({ visibility: 'hidden' });
  645. }, 200);
  646. }, 500);
  647. }
  648. else {
  649. this.changeData(-1);
  650. }
  651. };
  652. MainView.prototype.changeData = function (delta) {
  653. MainView.cleanLinks();
  654. //ignore out of bounds
  655. var newIndex = this.linkIndex + delta;
  656. if (newIndex > this.postData.length - 1 || newIndex < 0) {
  657. return;
  658. }
  659. if (this.postData[newIndex].tagTypeName !== this.mainImg.tagName || delta === 0) {
  660. this.mainImg = this.replaceElement(this.mainImg, this.postData[newIndex].tagTypeName);
  661. }
  662. //console.log('Opening: "' + this.postData[this.linkIndex].imgSrc +'" at index ' + this.linkIndex);
  663. this.mainImg.setAttr('src', this.postData[newIndex].imgSrc);
  664. var nextLinks = this.postData[newIndex].linksContainer;
  665. var nextQuote = this.postData[newIndex].quoteContainer;
  666. this.textWrapper.empty();
  667. this.textWrapper.append(nextLinks);
  668. this.textWrapper.append(nextQuote);
  669. this.linkIndex = newIndex;
  670. this.mainView.scrollToTop();
  671. MainView.setPersistentValue(INDEX_KEY, this.linkIndex);
  672. //update menu info
  673. this.pageDisplay.setText(this.linkIndex + 1);
  674. this.totalDisplay.setText(this.postData.length);
  675. this.nameDisplay.setText(this.postData[newIndex].imgSrc);
  676. };
  677. MainView.cleanLinks = function () {
  678. var links = document.getElementsByClassName('quotelink');
  679. for (var i = 0; i < links.length; ++i) {
  680. links[i].dispatchEvent(new MouseEvent('mouseout'));
  681. }
  682. };
  683. MainView.prototype.replaceElement = function (element, newTagType) {
  684. var _this = this;
  685. var rawElement = element.elementList[0];
  686. var newElement = Viewer.DomUtil.createElement(newTagType, {
  687. id: element.id,
  688. className: rawElement.className,
  689. style: rawElement.style,
  690. autoplay: true,
  691. controls: false,
  692. loop: true
  693. });
  694. newElement
  695. .on('click', function (event) {
  696. event.stopPropagation();
  697. _this.nextImg();
  698. })
  699. .on('load', function () {
  700. _this.imageLoadHandler();
  701. })
  702. .on('progress', function (e) {
  703. //console.log(e);
  704. });
  705. element.prepend(newElement);
  706. element.remove();
  707. return newElement;
  708. };
  709. MainView.prototype.eventStopper = function (event) {
  710. event.stopPropagation();
  711. if (event.target.nodeName === 'A') {
  712. var confirmed = this.confirmExit('Exit Viewer to navigate to link?');
  713. if (!confirmed) {
  714. event.preventDefault();
  715. }
  716. }
  717. };
  718. MainView.prototype.confirmExit = function (message) {
  719. var confirmed = window.confirm(message || 'Exit Viewer?');
  720. if (confirmed) {
  721. this.destroy();
  722. }
  723. return confirmed;
  724. };
  725. /* Removes the view and cleans up handlers*/
  726. MainView.prototype.destroy = function () {
  727. MainView.cleanLinks();
  728. UNSAFE_WINDOW.off();
  729. WINDOW.off();
  730. BODY.off();
  731. this.topLayer.remove();
  732. this.mainView.remove();
  733. this.customStyle.remove();
  734. this.bottomMenu.remove();
  735. BODY.setStyle({ overflow: 'auto' });
  736. this.canPreload = false;
  737. };
  738. /*Mouse-move Handler that watches for when menus should appear and mouse behavior*/
  739. MainView.prototype.menuWatcher = function (event) {
  740. var _this = this;
  741. var height_offset = window.innerHeight - this.bottomMenu.offsetHeight;
  742. var width_offset = window.innerWidth - this.bottomMenu.offsetWidth;
  743. var center = window.innerHeight / 2;
  744. var halfArrow = this.leftArrow.offsetHeight / 2;
  745. if (event.clientX >= width_offset && event.clientY >= height_offset) {
  746. this.bottomMenu.removeClass('hidden').addClass('bottomMenuShow');
  747. this.topMenu.removeClass('hidden').addClass('bottomMenuShow');
  748. }
  749. else if (this.bottomMenu.hasClass('bottomMenuShow')) {
  750. this.bottomMenu.removeClass('bottomMenuShow').addClass('hidden');
  751. this.topMenu.removeClass('bottomMenuShow').addClass('hidden');
  752. }
  753. if ((event.clientX <= (100) || event.clientX >= (window.innerWidth - 100)) &&
  754. (event.clientY <= (center + halfArrow) && event.clientY >= (center - halfArrow))) {
  755. this.rightArrow.removeClass('hidden');
  756. this.leftArrow.removeClass('hidden');
  757. }
  758. else {
  759. this.rightArrow.addClass('hidden');
  760. this.leftArrow.addClass('hidden');
  761. }
  762. //avoids chrome treating mouseclicks as mousemoves
  763. if (event.clientX !== this.lastMousePos.x && event.clientY !== this.lastMousePos.y) {
  764. //mouse click moves to next image when invisible
  765. this.mainImg.removeClass('hideCursor');
  766. window.clearTimeout(this.mouseTimer);
  767. BODY.off('click');
  768. BODY.removeClass('hideCursor');
  769. this.textWrapper.removeClass('disableClick');
  770. this.mainImg.removeClass('disableClick');
  771. this.centerBox.removeClass('disableClick');
  772. if (event.target.id === this.mainImg.id) {
  773. //hide cursor if it stops, show if it moves
  774. this.mouseTimer = window.setTimeout(function () {
  775. _this.mainImg.addClass('hideCursor');
  776. _this.textWrapper.addClass('disableClick');
  777. _this.mainImg.addClass('disableClick');
  778. _this.centerBox.addClass('disableClick');
  779. BODY.addClass('hideCursor')
  780. .on('click', function (event) {
  781. _this.windowClick(event);
  782. });
  783. }, 200);
  784. }
  785. }
  786. this.lastMousePos.x = event.clientX;
  787. this.lastMousePos.y = event.clientY;
  788. };
  789. /*Stores a key value pair as a cookie*/
  790. MainView.setPersistentValue = function (key, value) {
  791. document.cookie = key + '=' + value + ';expires=Thu, 01 Jan 3000 00:00:00 UTC;domain=.4chan.org;path=/';
  792. };
  793. /* Retrieves a cookie value via its key*/
  794. MainView.getPersistentValue = function (key) {
  795. var cookieMatch = document.cookie.match(new RegExp(key + '\\s*=\\s*([^;]+)'));
  796. if (cookieMatch) {
  797. return cookieMatch[1];
  798. }
  799. else {
  800. return undefined;
  801. }
  802. };
  803. MainView.prototype.setFitToScreenHeight = function (shouldFitImage) {
  804. this.shouldFitHeight = shouldFitImage;
  805. //ignore if image has no height as it is likely not loaded.
  806. if (shouldFitImage && this.mainImg.getAttr('naturalHeight')) {
  807. this.fitHeightToScreen();
  808. }
  809. else {
  810. this.mainImg.setStyle({ maxHeight: '' });
  811. }
  812. };
  813. ;
  814. MainView.prototype.setFitToScreenWidth = function (shouldFitImage) {
  815. this.mainImg.setStyle({
  816. maxWidth: shouldFitImage ? '100%' : 'none'
  817. });
  818. };
  819. MainView.prototype.imageLoadHandler = function () {
  820. if (this.shouldFitHeight) {
  821. this.fitHeightToScreen();
  822. }
  823. };
  824. /* Fits image to screen height*/
  825. MainView.prototype.fitHeightToScreen = function () {
  826. //sets the changeable properties to the image's real size
  827. var height = this.mainImg.getAttr('naturalHeight');
  828. this.mainImg.setStyle({ maxHeight: (height + 'px') });
  829. //actually tests if it is too high including padding
  830. var heightDiff = (this.mainImg.clientHeight > height) ?
  831. this.mainImg.clientHeight - this.mainView.clientHeight :
  832. height - this.mainView.clientHeight;
  833. if (heightDiff > 0) {
  834. this.mainImg.setStyle({ maxHeight: (height - heightDiff) + 'px' });
  835. }
  836. else {
  837. this.mainImg.setStyle({ maxHeight: (height + 'px') });
  838. }
  839. };
  840. return MainView;
  841. }());
  842. Viewer.MainView = MainView;
  843. })(Viewer || (Viewer = {}));
  844. var Viewer;
  845. (function (Viewer) {
  846. var TagType;
  847. (function (TagType) {
  848. TagType[TagType["IMG"] = 0] = "IMG";
  849. TagType[TagType["VIDEO"] = 1] = "VIDEO";
  850. })(TagType = Viewer.TagType || (Viewer.TagType = {}));
  851. var PostData = /** @class */ (function () {
  852. function PostData(imgSrc, quoteContainer, linksContainer, imageLink) {
  853. this.imgSrc = imgSrc;
  854. this.linksContainer = linksContainer;
  855. this.quoteContainer = quoteContainer;
  856. this.tagType = PostData.getElementType(imgSrc);
  857. this.imageLink = imageLink;
  858. }
  859. Object.defineProperty(PostData.prototype, "tagTypeName", {
  860. get: function () {
  861. return TagType[this.tagType];
  862. },
  863. enumerable: true,
  864. configurable: true
  865. });
  866. PostData.getElementType = function (src) {
  867. if (src.match(/\.(?:(?:webm)|(?:ogg)|(?:mp4))$/)) {
  868. return TagType.VIDEO;
  869. }
  870. else {
  871. return TagType.IMG;
  872. }
  873. };
  874. PostData.add4chanListenersToLinks = function (linkCollection) {
  875. linkCollection.find('.quotelink')
  876. .on('mouseover', Main.onThreadMouseOver)
  877. .on('mouseout', Main.onThreadMouseOut);
  878. };
  879. PostData.getImagePosts = function (asCopy) {
  880. var postData = [];
  881. var postFiles = Viewer.DomUtil.get('#delform').find('.postContainer');
  882. postFiles.each(function (post) {
  883. var _post = Viewer.DomUtil.get(post);
  884. var currentLinkTag = _post.find('.file .fileThumb');
  885. var currentLink = currentLinkTag.getAttr('href');
  886. if (!currentLink) {
  887. return;
  888. }
  889. var currentPostBlock = _post.find('.postMessage');
  890. var currentPostBacklinks = _post.find('.backlink');
  891. var newPostBlock = currentPostBlock;
  892. var newBackLinks = currentPostBacklinks;
  893. if (asCopy) {
  894. if (currentPostBlock.exists) {
  895. newPostBlock = currentPostBlock.lightClone();
  896. newPostBlock.addClass('viewerBlockQuote');
  897. PostData.add4chanListenersToLinks(newPostBlock);
  898. }
  899. if (currentPostBacklinks.exists) {
  900. newBackLinks = currentPostBacklinks.lightClone();
  901. newBackLinks.addClass('viewerBacklinks');
  902. PostData.add4chanListenersToLinks(newBackLinks);
  903. }
  904. }
  905. postData.push(new PostData(currentLink, newPostBlock, newBackLinks, currentLinkTag));
  906. });
  907. return postData;
  908. };
  909. return PostData;
  910. }());
  911. Viewer.PostData = PostData;
  912. })(Viewer || (Viewer = {}));
  913. /// <reference path="../MetaData.ts"/>
  914. /// <reference path="DomUtil.ts"/>
  915. /// <reference path="Css.ts"/>
  916. /// <reference path="MainView.ts"/>
  917. /// <reference path="PostData.ts"/>
  918. var Viewer;
  919. (function (Viewer) {
  920. function main() {
  921. // ========= Build the main Button ========= //
  922. Viewer.DomUtil.createElement('button')
  923. .setStyle({ position: 'fixed', bottom: '0', right: '0', })
  924. .html("Open Viewer")
  925. .on('click', function () {
  926. new Viewer.MainView();
  927. })
  928. .appendTo(document.body);
  929. // ========= Build buttons for each image thumbnail ========= //
  930. var posts = Viewer.PostData.getImagePosts(false);
  931. var imagePostCount = 0;
  932. for (var _i = 0, posts_1 = posts; _i < posts_1.length; _i++) {
  933. var post = posts_1[_i];
  934. Viewer.DomUtil.createElement('button')
  935. .setStyle({
  936. display: 'inline',
  937. float: 'left',
  938. clear: 'both',
  939. fontSize: '11px',
  940. cursor: 'pointer'
  941. })
  942. .setData({
  943. postIndex: imagePostCount
  944. })
  945. .html('Open Viewer')
  946. .on('click', function (e) {
  947. e.preventDefault();
  948. e.stopPropagation();
  949. //make the viewer and put it on the window so we can clean it up later
  950. new Viewer.MainView(parseInt(this.dataset.postIndex));
  951. })
  952. .appendTo(post.imageLink);
  953. ++imagePostCount;
  954. }
  955. }
  956. Viewer.main = main;
  957. })(Viewer || (Viewer = {}));
  958. //run the module
  959. Viewer.main();
  960. //# sourceMappingURL=viewer.js.map

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址