Image Viewer

Allows viewing full image without leaving the page

当前为 2022-04-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Image Viewer
  3. // @description Allows viewing full image without leaving the page
  4. // @namespace https://github.com/nikolay-borzov
  5. // @version 1.1.9
  6. // @author nikolay-borzov
  7. // @license MIT
  8. // @icon https://raw.githubusercontent.com/nikolay-borzov/user-scripts/master/image-viewer/icon.png
  9. // @homepageURL https://github.com/nikolay-borzov/user-scripts
  10. // @supportURL https://github.com/nikolay-borzov/user-scripts/issues
  11. // @include *
  12. // @connect www.imagebam.com
  13. // @connect imagevenue.com
  14. // @connect www.turboimagehost.com
  15. // @connect fastpic.ru
  16. // @connect radikal.ru
  17. // @run-at document-start
  18. // @compatible chrome
  19. // @compatible firefox
  20. // @grant GM_addStyle
  21. // @grant GM_xmlhttpRequest
  22. // @grant GM.xmlHttpRequest
  23. // @grant GM_setValue
  24. // @grant GM.setValue
  25. // @grant GM_getValue
  26. // @grant GM.getValue
  27. // @grant GM_registerMenuCommand
  28. // ==/UserScript==
  29. ;(function () {
  30. 'use strict'
  31.  
  32. /* global Bliss */
  33. // eslint-disable-next-line
  34. !(function(){function t(e,n,i){return n=void 0===n?1:n,i=i||n+1,i-n<=1?function(){if(arguments.length<=n||"string"===r.type(arguments[n]))return e.apply(this,arguments);var t,i=arguments[n];for(var o in i){var s=Array.prototype.slice.call(arguments);s.splice(n,1,o,i[o]),t=e.apply(this,s);}return t}:t(t(e,n+1,i),n,i-1)}function e(t,r,i){var o=n(i);if("string"===o){var s=Object.getOwnPropertyDescriptor(r,i);!s||s.writable&&s.configurable&&s.enumerable&&!s.get&&!s.set?t[i]=r[i]:(delete t[i],Object.defineProperty(t,i,s));}else if("array"===o)i.forEach(function(n){n in r&&e(t,r,n);});else for(var a in r)i&&("regexp"===o&&!i.test(a)||"function"===o&&!i.call(r,a))||e(t,r,a);return t}function n(t){if(null===t)return "null";if(void 0===t)return "undefined";var e=(Object.prototype.toString.call(t).match(/^\[object\s+(.*?)\]$/)[1]||"").toLowerCase();return "number"==e&&isNaN(t)?"nan":e}var r=self.Bliss=e(function(t,e){return 2==arguments.length&&!e||!t?null:"string"===r.type(t)?(e||document).querySelector(t):t||null},self.Bliss);e(r,{extend:e,overload:t,type:n,property:r.property||"_",listeners:self.WeakMap?new WeakMap:new Map,original:{addEventListener:(self.EventTarget||Node).prototype.addEventListener,removeEventListener:(self.EventTarget||Node).prototype.removeEventListener},sources:{},noop:function(){},$:function(t,e){return t instanceof Node||t instanceof Window?[t]:2!=arguments.length||e?Array.prototype.slice.call("string"==typeof t?(e||document).querySelectorAll(t):t||[]):[]},defined:function(){for(var t=0;t<arguments.length;t++)if(void 0!==arguments[t])return arguments[t]},create:function(t,e){return t instanceof Node?r.set(t,e):(1===arguments.length&&("string"===r.type(t)?e={}:(e=t,t=e.tag,e=r.extend({},e,function(t){return "tag"!==t}))),r.set(document.createElement(t||"div"),e))},each:function(t,e,n){n=n||{};for(var r in t)n[r]=e.call(t,r,t[r]);return n},ready:function(t,e,n){if("function"!=typeof t||e||(e=t,t=void 0),t=t||document,e&&("loading"!==t.readyState?e():r.once(t,"DOMContentLoaded",function(){e();})),!n)return new Promise(function(e){r.ready(t,e,!0);})},Class:function(t){var e,n=["constructor","extends","abstract","static"].concat(Object.keys(r.classProps)),i=t.hasOwnProperty("constructor")?t.constructor:r.noop;2==arguments.length?(e=arguments[0],t=arguments[1]):(e=function(){if(this.constructor.__abstract&&this.constructor===e)throw new Error("Abstract classes cannot be directly instantiated.");e["super"]&&e["super"].apply(this,arguments),i.apply(this,arguments);},e["super"]=t["extends"]||null,e.prototype=r.extend(Object.create(e["super"]?e["super"].prototype:Object),{constructor:e}),e.prototype["super"]=e["super"]?e["super"].prototype:null,e.__abstract=!!t["abstract"]);var o=function(t){return this.hasOwnProperty(t)&&n.indexOf(t)===-1};if(t["static"]){r.extend(e,t["static"],o);for(var s in r.classProps)s in t["static"]&&r.classProps[s](e,t["static"][s]);}r.extend(e.prototype,t,o);for(var s in r.classProps)s in t&&r.classProps[s](e.prototype,t[s]);return e},classProps:{lazy:t(function(t,e,n){return Object.defineProperty(t,e,{get:function(){var t=n.call(this);return Object.defineProperty(this,e,{value:t,configurable:!0,enumerable:!0,writable:!0}),t},set:function(t){Object.defineProperty(this,e,{value:t,configurable:!0,enumerable:!0,writable:!0});},configurable:!0,enumerable:!0}),t}),live:t(function(t,e,n){return "function"===r.type(n)&&(n={set:n}),Object.defineProperty(t,e,{get:function(){var t=this["_"+e],r=n.get&&n.get.call(this,t);return void 0!==r?r:t},set:function(t){var r=this["_"+e],i=n.set&&n.set.call(this,t,r);this["_"+e]=void 0!==i?i:t;},configurable:n.configurable,enumerable:n.enumerable}),t})},include:function(){var t=arguments[arguments.length-1],e=2===arguments.length&&arguments[0],n=document.createElement("script");return e?Promise.resolve():new Promise(function(e,i){r.set(n,{async:!0,onload:function(){e(),n.parentNode&&n.parentNode.removeChild(n);},onerror:function(){i();},src:t,inside:document.head});})},fetch:function(t,n){if(!t)throw new TypeError("URL parameter is mandatory and cannot be "+t);var i=e({url:new URL(t,location),data:"",method:"GET",headers:{},xhr:new XMLHttpRequest},n);i.method=i.method.toUpperCase(),r.hooks.run("fetch-args",i),"GET"===i.method&&i.data&&(i.url.search+=i.data),document.body.setAttribute("data-loading",i.url),i.xhr.open(i.method,i.url.href,i.async!==!1,i.user,i.password);for(var o in n)if("upload"===o)i.xhr.upload&&"object"==typeof n[o]&&r.extend(i.xhr.upload,n[o]);else if(o in i.xhr)try{i.xhr[o]=n[o];}catch(s){self.console&&console.error(s);}var a=Object.keys(i.headers).map(function(t){return t.toLowerCase()});"GET"!==i.method&&a.indexOf("content-type")===-1&&i.xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");for(var c in i.headers)void 0!==i.headers[c]&&i.xhr.setRequestHeader(c,i.headers[c]);var u=new Promise(function(t,e){i.xhr.onload=function(){document.body.removeAttribute("data-loading"),0===i.xhr.status||i.xhr.status>=200&&i.xhr.status<300||304===i.xhr.status?t(i.xhr):e(r.extend(Error(i.xhr.statusText),{xhr:i.xhr,get status(){return this.xhr.status}}));},i.xhr.onerror=function(){document.body.removeAttribute("data-loading"),e(r.extend(Error("Network Error"),{xhr:i.xhr}));},i.xhr.ontimeout=function(){document.body.removeAttribute("data-loading"),e(r.extend(Error("Network Timeout"),{xhr:i.xhr}));},i.xhr.send("GET"===i.method?null:i.data);});return u.xhr=i.xhr,u},value:function(t){var e="string"!==r.type(t);return r.$(arguments).slice(+e).reduce(function(t,e){return t&&t[e]},e?t:self)}}),r.Hooks=new r.Class({add:function(t,e,n){if("string"==typeof arguments[0])(Array.isArray(t)?t:[t]).forEach(function(t){this[t]=this[t]||[],e&&this[t][n?"unshift":"push"](e);},this);else for(var t in arguments[0])this.add(t,arguments[0][t],arguments[1]);},run:function(t,e){this[t]=this[t]||[],this[t].forEach(function(t){t.call(e&&e.context?e.context:e,e);});}}),r.hooks=new r.Hooks;r.property;r.Element=function(t){this.subject=t,this.data={},this.bliss={};},r.Element.prototype={set:t(function(t,e){t in r.setProps?r.setProps[t].call(this,e):t in this?this[t]=e:this.setAttribute(t,e);},0),transition:function(t,e){return e=+e||400,new Promise(function(n,i){if("transition"in this.style){var o=r.extend({},this.style,/^transition(Duration|Property)$/);r.style(this,{transitionDuration:(e||400)+"ms",transitionProperty:Object.keys(t).join(", ")}),r.once(this,"transitionend",function(){clearTimeout(s),r.style(this,o),n(this);});var s=setTimeout(n,e+50,this);r.style(this,t);}else r.style(this,t),n(this);}.bind(this))},fire:function(t,e){var n=document.createEvent("HTMLEvents");return n.initEvent(t,!0,!0),this.dispatchEvent(r.extend(n,e))},bind:t(function(t,e){if(arguments.length>1&&("function"===r.type(e)||e.handleEvent)){var n=e;e="object"===r.type(arguments[2])?arguments[2]:{capture:!!arguments[2]},e.callback=n;}var i=r.listeners.get(this)||{};t.trim().split(/\s+/).forEach(function(t){if(t.indexOf(".")>-1){t=t.split(".");var n=t[1];t=t[0];}i[t]=i[t]||[],0===i[t].filter(function(t){return t.callback===e.callback&&t.capture==e.capture}).length&&i[t].push(r.extend({className:n},e)),r.original.addEventListener.call(this,t,e.callback,e);},this),r.listeners.set(this,i);},0),unbind:t(function(t,e){if(e&&("function"===r.type(e)||e.handleEvent)){var n=e;e=arguments[2];}"boolean"==r.type(e)&&(e={capture:e}),e=e||{},e.callback=e.callback||n;var i=r.listeners.get(this);(t||"").trim().split(/\s+/).forEach(function(t){if(t.indexOf(".")>-1){t=t.split(".");var n=t[1];t=t[0];}if(t&&e.callback)return r.original.removeEventListener.call(this,t,e.callback,e.capture);if(i)for(var o in i)if(!t||o===t)for(var s,a=0;s=i[o][a];a++)n&&n!==s.className||e.callback&&e.callback!==s.callback||!!e.capture!=!!s.capture||(i[o].splice(a,1),r.original.removeEventListener.call(this,o,s.callback,s.capture),a--);},this);},0)},r.setProps={style:function(t){for(var e in t)e in this.style?this.style[e]=t[e]:this.style.setProperty(e,t[e]);},attributes:function(t){for(var e in t)this.setAttribute(e,t[e]);},properties:function(t){r.extend(this,t);},events:function(t){if(1!=arguments.length||!t||!t.addEventListener)return r.bind.apply(this,[this].concat(r.$(arguments)));var e=this;if(r.listeners){var n=r.listeners.get(t);for(var i in n)n[i].forEach(function(t){r.bind(e,i,t.callback,t.capture);});}for(var o in t)0===o.indexOf("on")&&(this[o]=t[o]);},once:t(function(t,e){var n=this,i=function(){return r.unbind(n,t,i),e.apply(n,arguments)};r.bind(this,t,i,{once:!0});},0),delegate:t(function(t,e,n){r.bind(this,t,function(t){t.target.closest(e)&&n.call(this,t);});},0,2),contents:function(t){(t||0===t)&&(Array.isArray(t)?t:[t]).forEach(function(t){var e=r.type(t);/^(string|number)$/.test(e)?t=document.createTextNode(t+""):"object"===e&&(t=r.create(t)),t instanceof Node&&this.appendChild(t);},this);},inside:function(t){t&&t.appendChild(this);},before:function(t){t&&t.parentNode.insertBefore(this,t);},after:function(t){t&&t.parentNode.insertBefore(this,t.nextSibling);},start:function(t){t&&t.insertBefore(this,t.firstChild);},around:function(t){t&&t.parentNode&&r.before(this,t),this.appendChild(t);}},r.Array=function(t){this.subject=t;},r.Array.prototype={all:function(t){var e=r.$(arguments).slice(1);return this[t].apply(this,e)}},r.add=t(function(t,e,n,i){n=r.extend({$:!0,element:!0,array:!0},n),"function"==r.type(e)&&(!n.element||t in r.Element.prototype&&i||(r.Element.prototype[t]=function(){return this.subject&&r.defined(e.apply(this.subject,arguments),this.subject)}),!n.array||t in r.Array.prototype&&i||(r.Array.prototype[t]=function(){var t=arguments;return this.subject.map(function(n){return n&&r.defined(e.apply(n,t),n)})}),n.$&&(r.sources[t]=r[t]=e,(n.array||n.element)&&(r[t]=function(){var e=[].slice.apply(arguments),i=e.shift(),o=n.array&&Array.isArray(i)?"Array":"Element";return r[o].prototype[t].apply({subject:i},e)})));},0),r.add(r.Array.prototype,{element:!1}),r.add(r.Element.prototype),r.add(r.setProps),r.add(r.classProps,{element:!1,array:!1});var i=document.createElement("_");r.add(r.extend({},HTMLElement.prototype,function(t){return "function"===r.type(i[t])}),null,!0);}());
  35.  
  36. // eslint-disable-next-line
  37. const $ = Bliss;
  38. // eslint-disable-next-line
  39. const $$ = Bliss.$;
  40.  
  41. function hasOwnProperty(object, property) {
  42. return Object.prototype.hasOwnProperty.call(object, property)
  43. }
  44.  
  45. const gmPolyfill = (function () {
  46. const gmMethodMap = {
  47. getValue: 'GM_getValue',
  48. setValue: 'GM_setValue',
  49. openInTab: 'GM_openInTab',
  50. }
  51.  
  52. return function polyfill(methodName) {
  53. if (hasOwnProperty(gmMethodMap, methodName)) {
  54. return typeof GM !== 'undefined' && methodName in GM
  55. ? GM[methodName]
  56. : function (...args) {
  57. return new Promise((resolve, reject) => {
  58. try {
  59. resolve(window[gmMethodMap[methodName]](...args))
  60. } catch (e) {
  61. reject(e)
  62. }
  63. })
  64. }
  65. }
  66.  
  67. return null
  68. }
  69. })()
  70.  
  71. const addStyle =
  72. 'GM_addStyle' in window
  73. ? GM_addStyle // eslint-disable-line camelcase
  74. : (css) => {
  75. const head = document.getElementsByTagName('head')[0]
  76.  
  77. if (head) {
  78. const style = document.createElement('style')
  79.  
  80. style.type = 'text/css'
  81. style.innerHTML = css
  82. head.appendChild(style)
  83.  
  84. return css
  85. }
  86. }
  87.  
  88. let request$1
  89.  
  90. const getRequest = () => {
  91. if (!request$1) {
  92. const xmlHttpRequest =
  93. typeof GM !== 'undefined' && 'xmlHttpRequest' in GM
  94. ? GM.xmlHttpRequest
  95. : GM_xmlhttpRequest; // eslint-disable-line
  96.  
  97. request$1 = function (url, { method = 'GET' } = {}) {
  98. return new Promise((resolve, reject) => {
  99. xmlHttpRequest({
  100. url,
  101. method,
  102. onload: resolve,
  103. onerror: reject,
  104. })
  105. })
  106. }
  107. }
  108. return request$1
  109. }
  110.  
  111. let store
  112.  
  113. const getStore = () => {
  114. if (!store) {
  115. store = {
  116. get: gmPolyfill('getValue'),
  117. set: gmPolyfill('setValue'),
  118. }
  119. }
  120.  
  121. return store
  122. }
  123.  
  124. const request = getRequest()
  125.  
  126. async function getPageHtml(pageUrl) {
  127. const response = await request(pageUrl)
  128.  
  129. return response.responseText
  130. }
  131.  
  132. async function getUrlFromPage(link, extractor) {
  133. const html = await getPageHtml(link.url)
  134.  
  135. const match = extractor.imageUrlRegEx.exec(html)
  136.  
  137. let url
  138.  
  139. if (match) {
  140. if (match.groups) {
  141. url = match.groups.url
  142. } else {
  143. url = match[1]
  144. }
  145. }
  146.  
  147. if (!url) {
  148. console.warn(`[image-viewer] Unable to get URL from page ${link.url}`)
  149. }
  150.  
  151. return url
  152. }
  153.  
  154. const fastpic = {
  155. name: 'FastPic',
  156. linkRegEx: /^http.?:\/\/fastpic\.ru\/view/,
  157. imageUrlRegEx: /src="(?<url>[^"]+)" class="image img-fluid"/,
  158. getUrl: getUrlFromPage,
  159. }
  160.  
  161. const URL_PARTS_REGEXP = /i(\d+).+big(\/\d+\/\d+\/).+\/([^/]+)$/
  162.  
  163. const fastpicDirect = {
  164. name: 'FastPic (direct link)',
  165. linkRegEx: /fastpic\.ru\/big/,
  166.  
  167. async getUrl(link) {
  168. let hostLink = link.url
  169.  
  170. if (hostLink.includes('?')) {
  171. const urlObject = new URL(hostLink)
  172. const params = new URLSearchParams(urlObject.search)
  173. for (const param of params.values()) {
  174. if (fastpicDirect.linkRegEx.test(param)) {
  175. hostLink = param
  176. break
  177. }
  178. }
  179. }
  180.  
  181. const [, index, date, filename] = URL_PARTS_REGEXP.exec(hostLink)
  182.  
  183. const url = `https://fastpic.ru/view/${index}${date}${filename}.html`
  184.  
  185. return fastpic.getUrl({ ...link, url }, fastpic)
  186. },
  187. }
  188.  
  189. const imagebam = {
  190. name: 'ImageBam',
  191. linkRegEx: /^http:\/\/www\.imagebam\.com\/image/,
  192. imageUrlRegEx: /property="og:image" content="([^"]*)"/,
  193. getUrl: getUrlFromPage,
  194. }
  195.  
  196. const DATE_PATTERN = /(\d{4})\.(\d{2})\.(\d{2})/
  197.  
  198. const imageban = {
  199. name: 'ImageBan.ru',
  200. linkRegEx: /\/\/imageban\.ru\/show/,
  201.  
  202. async getUrl(link) {
  203. return link.thumbnailUrl
  204. .replace('thumbs', 'out')
  205. .replace(DATE_PATTERN, '$1/$2/$3')
  206. },
  207. }
  208.  
  209. const imagebanDirect = {
  210. name: 'ImageBan.ru (direct link)',
  211. linkRegEx: /imageban\.ru\/out/,
  212.  
  213. async getUrl(link) {
  214. return link.url
  215. },
  216. }
  217.  
  218. const imagetwist = {
  219. name: 'ImageTwist',
  220. linkRegEx: /imagetwist\.com/,
  221.  
  222. async getUrl(link) {
  223. const imageName = link.url.split('/').pop().replace('.html', '')
  224. const extension = imageName.split('.').pop()
  225. const imageUrl = link.thumbnailUrl
  226. .replace('/th/', '/i/')
  227. .slice(0, -extension.length)
  228.  
  229. return `${imageUrl}${extension}/${imageName}`
  230. },
  231. }
  232.  
  233. const HOST_REPLACE_REG_EX$2 = /(picturelol|picshick|imageshimage)/
  234.  
  235. const imagetwistBased = {
  236. name: 'ImageTwist based (legacy)',
  237. hosts: ['Picturelol.com', 'PicShick.com', 'Imageshimage.com'],
  238. linkRegEx: /^https?:\/\/(picturelol|picshick|imageshimage)\.com/,
  239.  
  240. async getUrl(link) {
  241. const imageName = link.url.split('/').pop()
  242. const imageUrl = link.thumbnailUrl
  243. .replace('/th/', '/i/')
  244. .replace(HOST_REPLACE_REG_EX$2, 'imagetwist')
  245.  
  246. return `${imageUrl}/${imageName}`
  247. },
  248. }
  249.  
  250. const imagevenueLegacy = {
  251. name: 'ImageVenue.com',
  252.  
  253. linkRegEx: /(imagevenue.com\/img.php|www.imagevenue.com\/\\w+$)/,
  254. imageUrlRegEx: /data-toggle="full">\W*<img src="(?<url>[^"]*)/im,
  255.  
  256. getUrl: getUrlFromPage,
  257. }
  258.  
  259. const imgadult = {
  260. name: 'ImgAdult.com',
  261. linkRegEx: /^https:\/\/imgadult\.com/,
  262.  
  263. async getUrl(link) {
  264. return link.thumbnailUrl.replace('/small/', '/big/')
  265. },
  266. }
  267.  
  268. const imgbb = {
  269. name: 'imgbb.com',
  270. linkRegEx: /^https:\/\/ibb\.co/,
  271.  
  272. async getUrl(link) {
  273. return link.thumbnailUrl.replace('//thumb', '//image')
  274. },
  275. }
  276.  
  277. const imgbox = {
  278. name: 'imgbox.com',
  279. linkRegEx: /^http:\/\/imgbox\.com/,
  280.  
  281. async getUrl(link) {
  282. return link.thumbnailUrl.replace('/thumbs', '/images').replace('_t', '_o')
  283. },
  284. }
  285.  
  286. const imgbum = {
  287. name: 'imgbum.net',
  288. linkRegEx: /^http:\/\/imgbum\.net/,
  289.  
  290. async getUrl(link) {
  291. return link.thumbnailUrl.replace('-thumb', '')
  292. },
  293. }
  294.  
  295. const imgchilibum = {
  296. name: 'imgchilibum.ru',
  297. linkRegEx: /^http:\/\/imgchilibum\.ru\/v/,
  298.  
  299. async getUrl(link) {
  300. return link.thumbnailUrl.replace('_s/', '_b/')
  301. },
  302. }
  303.  
  304. const imgdrive = {
  305. name: 'ImgDrive.net',
  306. linkRegEx: /^https:\/\/imgdrive\.net/,
  307.  
  308. async getUrl(link) {
  309. return link.thumbnailUrl.replace('small', 'big')
  310. },
  311. }
  312.  
  313. const imgtaxi = {
  314. name: 'imgtaxi.com',
  315. linkRegEx: /^https:\/\/imgtaxi\.com/,
  316.  
  317. async getUrl(link) {
  318. return link.thumbnailUrl
  319. .replace('/small/', '/big/')
  320. .replace('/small-medium/', '/big/')
  321. },
  322. }
  323.  
  324. const imx = {
  325. name: 'IMX.to',
  326. linkRegEx: /^https:\/\/imx\.to/,
  327.  
  328. async getUrl(link) {
  329. return link.thumbnailUrl.replace('/imx', '/i.imx').replace('/u/t/', '/i/')
  330. },
  331. }
  332.  
  333. const lostpic = {
  334. name: 'Lostpic.net',
  335. linkRegEx: /^http:\/\/lostpic\.net/,
  336.  
  337. async getUrl(link) {
  338. return link.thumbnailUrl.replace('.th', '').replace('http:', 'https:')
  339. },
  340. }
  341.  
  342. const moneyPic = {
  343. name: 'money-pic.ru',
  344. linkRegEx: /^http:\/\/money-pic\.ru/,
  345.  
  346. async getUrl(link) {
  347. return link.thumbnailUrl.replace('-thumb', '')
  348. },
  349. }
  350.  
  351. const nikapic = {
  352. name: 'nikapic.ru',
  353. linkRegEx: /^http:\/\/nikapic\.ru/,
  354.  
  355. async getUrl(link) {
  356. return link.thumbnailUrl.replace('/small/', '/big/')
  357. },
  358. }
  359.  
  360. const picage = {
  361. name: 'picage.ru',
  362. linkRegEx: /^http:\/\/picage\.ru/,
  363.  
  364. async getUrl(link) {
  365. return link.thumbnailUrl
  366. .replace('picage', 'pic4you')
  367. .replace('-thumb', '')
  368. },
  369. }
  370.  
  371. const piccash = {
  372. name: 'PicCash.net',
  373. linkRegEx: /^http:\/\/piccash\.net\//,
  374.  
  375. async getUrl(link) {
  376. return link.thumbnailUrl.replace('_thumb', '_full').replace('-thumb', '')
  377. },
  378. }
  379.  
  380. const HOST_REPLACE_REG_EX$1 =
  381. /(freescreens\.ru|imgclick\.ru|picclick\.ru|payforpic\.ru|picforall\.ru)/
  382.  
  383. const picforall = {
  384. name: 'PicForAll.ru',
  385. hosts: [
  386. 'freescreens.ru',
  387. 'imgclick.ru',
  388. 'picclick.ru',
  389. 'payforpic.ru',
  390. 'picforall.ru',
  391. ],
  392. linkRegEx:
  393. /^http:\/\/(freescreens\.ru|imgclick\.ru|picclick\.ru|payforpic\.ru|picforall\.ru)/,
  394.  
  395. async getUrl(link) {
  396. return link.thumbnailUrl
  397. .replace(HOST_REPLACE_REG_EX$1, 'picpic.online')
  398. .replace('-thumb', '')
  399. },
  400. }
  401.  
  402. const HOST_REPLACE_REG_EX =
  403. /(iceimg\.net|pixsense\.net|vestimage\.site|chaosimg\.site)/
  404.  
  405. const pixsense = {
  406. name: 'PixSense',
  407. hosts: [
  408. 'www.iceimg.net',
  409. 'www.pixsense.net',
  410. 'www.vestimage.site',
  411. 'www.chaosimg.site',
  412. ],
  413. linkRegEx:
  414. /^http:\/\/www\.(iceimg\.net|pixsense\.net|vestimage\.site|chaosimg\.site)/,
  415.  
  416. async getUrl(link) {
  417. return link.thumbnailUrl
  418. .replace(HOST_REPLACE_REG_EX, 'fortstore.net')
  419. .replace('small-', '')
  420. .replace('/small/', '/big/')
  421. },
  422. }
  423.  
  424. const radikal = {
  425. name: 'Radikal.ru',
  426. linkRegEx: /https?:\/\/.\.radikal\.ru\//,
  427.  
  428. async getUrl(link) {
  429. return link.url
  430. },
  431. }
  432.  
  433. const radikalLegacy = {
  434. name: 'Radikal.ru (legacy)',
  435. linkRegEx: /^http:\/\/radikal\.ru\//,
  436. imageUrlRegEx: /id="imgFullSize" src="(?<url>[^"]+)"/,
  437. getUrl: getUrlFromPage,
  438. }
  439.  
  440. const stuffed = {
  441. name: 'stuffed.ru',
  442. linkRegEx: /^http:\/\/stuffed\.ru/,
  443.  
  444. async getUrl(link) {
  445. return link.thumbnailUrl.replace('-thumb', '')
  446. },
  447. }
  448.  
  449. const turboimagehost = {
  450. name: 'TurboImageHost',
  451. linkRegEx: /^https:\/\/www\.turboimagehost\.com\/p/,
  452. imageUrlRegEx: /property="og:image" content="([^"]*)"/,
  453. getUrl: getUrlFromPage,
  454. }
  455.  
  456. const vfl = {
  457. name: 'VFL.ru',
  458. linkRegEx: /^http:\/\/vfl\.ru/,
  459.  
  460. async getUrl(link) {
  461. return link.thumbnailUrl.replace('_s', '')
  462. },
  463. }
  464.  
  465. const xxxscreens = {
  466. name: 'XXXScreens.com',
  467. linkRegEx: /^http:\/\/xxxscreens\.com/,
  468.  
  469. async getUrl(link) {
  470. return link.thumbnailUrl.replace('small/', 'big/')
  471. },
  472. }
  473.  
  474. const hostExtractors = /* #__PURE__ */ Object.freeze({
  475. __proto__: null,
  476. fastpic: fastpic,
  477. fastpicDirect: fastpicDirect,
  478. imagebam: imagebam,
  479. imageban: imageban,
  480. imagebanDirect: imagebanDirect,
  481. imagetwist: imagetwist,
  482. imagetwistBased: imagetwistBased,
  483. imagevenueLegacy: imagevenueLegacy,
  484. imgadult: imgadult,
  485. imgbb: imgbb,
  486. imgbox: imgbox,
  487. imgbum: imgbum,
  488. imgchilibum: imgchilibum,
  489. imgdrive: imgdrive,
  490. imgtaxi: imgtaxi,
  491. imx: imx,
  492. lostpic: lostpic,
  493. moneyPic: moneyPic,
  494. nikapic: nikapic,
  495. picage: picage,
  496. piccash: piccash,
  497. picforall: picforall,
  498. pixsense: pixsense,
  499. radikal: radikal,
  500. radikalLegacy: radikalLegacy,
  501. stuffed: stuffed,
  502. turboimagehost: turboimagehost,
  503. vfl: vfl,
  504. xxxscreens: xxxscreens,
  505. })
  506.  
  507. const urlExtractor = (function () {
  508. function sortCaseInsensitive(array, getValue) {
  509. return array
  510. .map((value, index) => ({
  511. index,
  512. value: getValue(value).toLowerCase(),
  513. }))
  514. .sort((a, b) => {
  515. if (a.value > b.value) {
  516. return 1
  517. }
  518. if (a.value < b.value) {
  519. return -1
  520. }
  521. return 0
  522. })
  523. .map((m) => array[m.index])
  524. }
  525.  
  526. let extractorsActive = []
  527.  
  528. const extractors = Object.values(hostExtractors).filter(Boolean)
  529.  
  530. const extractorsByName = extractors.reduce((result, extractor) => {
  531. result[extractor.name] = extractor
  532. return result
  533. }, {})
  534.  
  535. return {
  536. getImageHostsInfo() {
  537. const result = extractors.map((e) => ({
  538. name: e.name,
  539. description: e.hosts ? e.hosts.join(', ') : '',
  540. }))
  541.  
  542. return sortCaseInsensitive(result, (value) => value.name)
  543. },
  544.  
  545. getImageUrl(link) {
  546. const extractor = extractorsByName[link.host]
  547.  
  548. return extractor.getUrl(link, extractor)
  549. },
  550.  
  551. getHostNameMatcher(enabledHosts) {
  552. extractorsActive = extractors.filter((e) =>
  553. enabledHosts.includes(e.name)
  554. )
  555.  
  556. let prevExtractor = null
  557.  
  558. return (url) => {
  559. if (prevExtractor && prevExtractor.linkRegEx.test(url)) {
  560. return prevExtractor.name
  561. }
  562.  
  563. const extractor = extractorsActive.find((e) => e.linkRegEx.test(url))
  564.  
  565. if (extractor) {
  566. prevExtractor = extractor
  567. return extractor.name
  568. }
  569.  
  570. return null
  571. }
  572. },
  573. }
  574. })()
  575.  
  576. const config = (function () {
  577. const store = getStore()
  578.  
  579. const CLASSES = {
  580. open: 'iv-config-form--open',
  581. }
  582.  
  583. let configMenu = null
  584. const currentHost = unsafeWindow.location.host
  585.  
  586. function showMenu(config) {
  587. createMenuElement(config).classList.add(CLASSES.open)
  588. }
  589.  
  590. function createMenuElement(config) {
  591. if (!configMenu) {
  592. const rows = config.hosts.map(createConfigMenuRow)
  593.  
  594. configMenu = $.create('div', {
  595. id: 'iv-config-form',
  596. className: 'iv-config-form',
  597. contents: [
  598. createMenuHeader(),
  599. {
  600. tag: 'div',
  601. className: 'iv-config-form__options',
  602. contents: rows,
  603. },
  604. ],
  605. delegate: {
  606. change: {
  607. '.js-iv-config-checkbox': (e) =>
  608. updateHostConfig(
  609. config.storedConfig,
  610. e.target.value,
  611. e.target.checked
  612. ),
  613. },
  614. },
  615. })
  616.  
  617. document.body.appendChild(configMenu)
  618. }
  619.  
  620. return configMenu
  621. }
  622.  
  623. function createMenuHeader() {
  624. const closeButton = $.create('a', {
  625. href: '#',
  626. title: 'Close',
  627. className: `iv-icon-button iv-icon-button--small iv-icon iv-icon--type-close`,
  628. events: {
  629. click: (e) => {
  630. e.preventDefault()
  631. configMenu.classList.remove(CLASSES.open)
  632. },
  633. },
  634. })
  635.  
  636. return {
  637. tag: 'div',
  638. className: 'iv-config-form__header',
  639. contents: [
  640. {
  641. tag: 'span',
  642. className: 'iv-config-form__header-title',
  643. contents: `Settings for ${currentHost}`,
  644. },
  645. closeButton,
  646. ],
  647. }
  648. }
  649.  
  650. function createConfigMenuRow(host) {
  651. return $.create('label', {
  652. className: 'iv-config-form__label',
  653. title: host.description,
  654. contents: [
  655. {
  656. tag: 'input',
  657. type: 'checkbox',
  658. className: 'iv-config-form__checkbox js-iv-config-checkbox',
  659. checked: host.enabled,
  660. value: host.name,
  661. },
  662. host.name,
  663. ],
  664. })
  665. }
  666.  
  667. function updateHostConfig(config, hostName, isEnabled) {
  668. config.hosts[hostName] = isEnabled
  669. store.set(currentHost, config)
  670. }
  671.  
  672. async function getHostConfig() {
  673. const hosts = urlExtractor.getImageHostsInfo()
  674. const storedConfig = await store.get(currentHost, { hosts: {} })
  675. const enabledHosts = []
  676.  
  677. hosts.forEach((host) => {
  678. const id = host.name
  679. const isEnabled =
  680. id in storedConfig.hosts ? storedConfig.hosts[id] : true
  681.  
  682. host.enabled = isEnabled
  683. storedConfig.hosts[id] = isEnabled
  684.  
  685. if (isEnabled) {
  686. enabledHosts.push(id)
  687. }
  688. })
  689.  
  690. storedConfig.hosts = hosts.reduce((result, host) => {
  691. result[host.name] = host.enabled
  692. return result
  693. }, {})
  694.  
  695. return {
  696. hosts,
  697. storedConfig,
  698. enabledHosts,
  699. }
  700. }
  701.  
  702. return {
  703. async init() {
  704. const config = await getHostConfig()
  705.  
  706. const handler = () => showMenu(config)
  707.  
  708. // eslint-disable-next-line
  709. if (typeof GM_registerMenuCommand !== 'undefined') {
  710. GM_registerMenuCommand('Image Viewer Settings', handler)
  711. } else {
  712. unsafeWindow.imageViewer = {
  713. settings: handler,
  714. }
  715. }
  716.  
  717. return config
  718. },
  719. }
  720. })()
  721.  
  722. const css_248z =
  723. "@keyframes spin{0%{transform:translate(-50%,-50%) rotate(0deg)}to{transform:translate(-50%,-50%) rotate(1turn)}}.iv-icon{position:relative}.iv-icon:after,.iv-image-link img:after{background-position:50%;background-repeat:no-repeat;background-size:contain;content:\"\";height:100%;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:100%;z-index:2}.iv-icon--hover:after{opacity:0;transition:opacity .35s ease}.iv-icon--hover:hover:after{opacity:1}.iv-icon--size-button:after{height:50px;width:50px}.iv-icon--type-loading:after{animation:spin 1s linear infinite;background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff'%3E%3Cpath d='M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z'/%3E%3C/svg%3E\")!important;opacity:1}.iv-icon--type-zoom:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg fill='%23fff' height='24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z'/%3E%3C/svg%3E\")}.iv-icon--type-next:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg fill='%23fff' height='24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E\")}.iv-icon--type-previous:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg fill='%23fff' height='24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.41 7.41 14 6l-6 6 6 6 1.41-1.41L10.83 12z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E\")}.iv-icon--type-close:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg fill='%23fff' height='24' width='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E\")}.iv-icon--type-expand:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff'%3E%3Cpath d='M5 5h5v2H7v3H5V5m9 0h5v5h-2V7h-3V5m3 9h2v5h-5v-2h3v-3m-7 3v2H5v-5h2v3h3z'/%3E%3C/svg%3E\")}.iv-icon--type-shrink:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff'%3E%3Cpath d='M14 14h5v2h-3v3h-2v-5m-9 0h5v5H8v-3H5v-2m3-9h2v5H5V8h3V5m11 3v2h-5V5h2v3h3z'/%3E%3C/svg%3E\")}.iv-icon--type-image-broken:after,.iv-image-link img:after{background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff'%3E%3Cpath d='M21 5v6.59l-3-3.01-4 4.01-4-4-4 4-3-3.01V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2m-3 6.42 3 3.01V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6.58l3 2.99 4-4 4 4'/%3E%3C/svg%3E\")}.iv-image-link{border:1px solid rgba(0,0,0,.2);box-shadow:1px 1px 3px rgba(0,0,0,.5);display:inline-flex;margin:3px;min-height:50px;min-width:50px;padding:4px;vertical-align:top}.iv-image-link img{margin:0}.iv-image-link>:not(img){align-items:center;display:flex;justify-content:center;width:100%}.iv-image-link:before{background-color:rgba(0,0,0,.5);bottom:4px;content:\"\";left:4px;opacity:0;position:absolute;right:4px;top:4px;transition:opacity .35s ease;z-index:1}.iv-image-link.iv-icon--type-loading:before,.iv-image-link:hover:before{opacity:1}.iv-image-link img:after,.iv-image-link img:before{content:\"\";position:absolute}.iv-image-link img:before{background-color:rgba(0,0,0,.2);height:100%;left:0;top:0;width:100%}.iv-image-link img:after{height:35px;width:35px;z-index:0}.iv-image-view{background-color:rgba(0,0,0,.8);color:#fff;display:none;flex-direction:column;height:0;opacity:0;transition:opacity .35s ease-out;user-select:none}.iv-image-view--open body,html.iv-image-view--open{overflow:hidden}.iv-image-view--open .iv-image-view{bottom:0;display:flex;height:auto;left:0;opacity:1;position:fixed;right:0;top:0;z-index:3}.iv-image-view--single .single-hide{visibility:hidden}.iv-image-view__footer,.iv-image-view__header{background-color:rgba(0,0,0,.8);display:flex}.iv-image-view__footer-wrapper,.iv-image-view__header-wrapper{z-index:2}.iv-image-view__header-wrapper{box-shadow:0 3px 7px rgba(0,0,0,.7)}.iv-image-view__footer-wrapper{box-shadow:0 -3px 7px rgba(0,0,0,.7)}.iv-image-view__header{justify-content:space-between}.iv-image-view__footer{justify-content:center}.iv-image-view__body{display:flex;height:100%;overflow:auto;position:relative}.iv-image-view__body::-webkit-scrollbar{width:20px}.iv-image-view__body::-webkit-scrollbar-thumb{background-color:rgba(0,0,0,.8)}.iv-image-view__body::-webkit-scrollbar-track{background-color:hsla(0,0%,100%,.8)}.iv-thumbnail-wrapper{display:flex;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0}.iv-image-view__number{align-items:center;display:flex;font-size:18px;padding:0 40px}.iv-image-view__backdrop{height:100%;left:0;position:fixed;top:0;width:100%;z-index:1}.iv-image,.iv-thumbnail{margin:auto;max-height:100%;max-width:100%;object-fit:contain}.iv-image{opacity:1;transition:opacity .35s ease-out;z-index:2}.iv-thumbnail{filter:blur(5px)}.iv-icon--type-error .iv-image,.iv-image-view__image--loading .iv-image,.iv-image-view__image--thumbnail .iv-image{opacity:0}.iv-image-view__image--thumbnail .iv-thumbnail-wrapper{z-index:2}.iv-image-view--full-height .iv-image,.iv-image-view--full-height .iv-thumbnail{cursor:grab;max-height:none}.iv-image-view--full-height .iv-image--grabbing{cursor:grabbing}.iv-icon-button{height:50px;transition:all .35s ease-out;width:50px}.iv-icon-button--small{height:25px;width:25px}.iv-icon-button+.iv-icon-button{margin-left:5px}.iv-icon-button:hover{background-color:hsla(0,0%,100%,.1)}.iv-icon-button--active,.iv-icon-button:active{background-color:hsla(0,0%,100%,.2)}.iv-config-form{background-color:rgba(0,0,0,.85);color:#fff;display:none;flex-direction:column;height:50%;left:10px;max-width:500px;padding:10px;top:10px;width:50%}.iv-config-form--open{display:flex;position:fixed;z-index:3}.iv-config-form__header{align-items:center;display:flex;padding:10px}.iv-config-form__header-title{flex-grow:1}.iv-config-form__options{display:flex;flex-flow:column wrap;flex-grow:1;overflow:auto}.iv-config-form__label{align-items:center;display:flex;flex:0 0 auto;margin:0;padding:10px;transition:all .35s ease-out}.iv-config-form__label:hover{background-color:hsla(0,0%,100%,.15)}.iv-config-form__checkbox{margin:0 5px 0 0!important}"
  724.  
  725. const initViewer = (function () {
  726. const CLASSES = {
  727. imageLink: 'js-image-link',
  728. imageLinkZoom: 'iv-icon--type-zoom',
  729. imageLinkHover: 'iv-icon--hover',
  730. brokenImage: 'iv-icon--type-image-broken',
  731. loadingIcon: 'iv-icon--type-loading',
  732. loading: 'iv-image-view__image--loading',
  733. thumbnail: 'iv-image-view__image--thumbnail',
  734. open: 'iv-image-view--open',
  735. single: 'iv-image-view--single',
  736. fullHeight: 'iv-image-view--full-height',
  737. iconExpand: 'iv-icon--type-expand',
  738. iconShrink: 'iv-icon--type-shrink',
  739. grabbing: 'iv-image--grabbing',
  740. buttonActive: 'iv-icon-button--active',
  741. }
  742.  
  743. const SELECTORS = {
  744. imageLink: `.${CLASSES.imageLink}`,
  745. }
  746.  
  747. const EMPTY_SRC =
  748. 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAI='
  749.  
  750. const TRANSITION_DURATION = 350
  751.  
  752. const elements = {
  753. container: null,
  754. image: null,
  755. imageThumbnail: null,
  756. imageContainer: null,
  757. imageNumber: null,
  758. imageTotal: null,
  759. buttons: {
  760. next: null,
  761. previous: null,
  762. close: null,
  763. toggleFullHeight: null,
  764. },
  765. }
  766.  
  767. const state = {
  768. firstClick: true,
  769. open: false,
  770. currentLink: null,
  771. linksSet: [],
  772. isSingle: false,
  773. getCurrentLinkIndex() {
  774. return this.linksSet.indexOf(this.currentLink)
  775. },
  776. getLastLinkIndex() {
  777. return this.linksSet.length - 1
  778. },
  779. dragPosition: null,
  780. dragging: false,
  781. }
  782.  
  783. const image = {
  784. async show(link) {
  785. const container = elements.container
  786. const img = elements.image
  787. const thumbnail = elements.imageThumbnail
  788.  
  789. state.currentLink = link
  790.  
  791. if (state.isSingle) {
  792. container.classList.add(CLASSES.single)
  793. } else {
  794. container.classList.remove(CLASSES.single)
  795. elements.imageNumber.textContent = state.getCurrentLinkIndex() + 1
  796. }
  797.  
  798. if (!state.open) {
  799. document.documentElement.classList.add(CLASSES.open)
  800. state.open = true
  801. }
  802.  
  803. img.src = EMPTY_SRC
  804.  
  805. if (link.classList.contains(CLASSES.brokenImage)) {
  806. container.classList.add(CLASSES.brokenImage)
  807.  
  808. return
  809. }
  810.  
  811. container.classList.remove(CLASSES.brokenImage)
  812.  
  813. container.classList.add(CLASSES.loading, CLASSES.loadingIcon)
  814.  
  815. const isSizeKnown = !!link.dataset.ivWidth
  816. const thumbnailUrl = link.dataset.ivThumbnail
  817.  
  818. if (isSizeKnown) {
  819. thumbnail.width = link.dataset.ivWidth
  820. thumbnail.src = thumbnailUrl
  821.  
  822. container.classList.add(CLASSES.thumbnail)
  823. }
  824.  
  825. let imageUrl = link.dataset.ivImgUrl
  826.  
  827. if (!imageUrl) {
  828. imageUrl = await urlExtractor.getImageUrl({
  829. url: link.href,
  830. thumbnailUrl,
  831. host: link.dataset.ivHost,
  832. })
  833.  
  834. if (!imageUrl) {
  835. image.markAsBroken(link)
  836. return
  837. }
  838.  
  839. link.dataset.ivImgUrl = imageUrl
  840. }
  841.  
  842. try {
  843. await image.preload(
  844. imageUrl,
  845. isSizeKnown ? null : image.setThumbnailSize
  846. )
  847.  
  848. img.src = imageUrl
  849.  
  850. container.classList.remove(
  851. CLASSES.thumbnail,
  852. CLASSES.loading,
  853. CLASSES.loadingIcon
  854. )
  855.  
  856. setTimeout(image.hideThumbnail, TRANSITION_DURATION)
  857. } catch (e) {
  858. link.classList.remove(CLASSES.imageLink)
  859. image.markAsBroken(link)
  860.  
  861. $.attributes(link, { target: '_blank' })
  862. }
  863. },
  864.  
  865. preload(url, onSizeGet) {
  866. return new Promise((resolve, reject) => {
  867. const imageObject = new Image()
  868.  
  869. imageObject.onload = resolve
  870. imageObject.onerror = reject
  871.  
  872. imageObject.src = url
  873.  
  874. if (onSizeGet) {
  875. image.getSize(imageObject).then(onSizeGet)
  876. }
  877. })
  878. },
  879.  
  880. getSize(img) {
  881. return new Promise((resolve) => {
  882. const intervalId = setInterval(() => {
  883. if (img.naturalWidth) {
  884. clearInterval(intervalId)
  885. resolve({ width: img.naturalWidth, complete: img.complete })
  886. }
  887. }, 10)
  888. })
  889. },
  890.  
  891. setThumbnailSize({ width, complete }) {
  892. elements.imageThumbnail.width = width
  893. elements.imageThumbnail.src = state.currentLink.dataset.ivThumbnail
  894.  
  895. if (!complete) {
  896. elements.container.classList.add(CLASSES.thumbnail)
  897. }
  898.  
  899. state.currentLink.dataset.ivWidth = width
  900. },
  901.  
  902. hideThumbnail() {
  903. elements.imageThumbnail.removeAttribute('width')
  904. elements.imageThumbnail.src = EMPTY_SRC
  905. },
  906.  
  907. hide() {
  908. document.documentElement.classList.remove(CLASSES.open)
  909. state.open = false
  910. state.currentLink = null
  911. elements.image.src = EMPTY_SRC
  912. events.keyboard.unbind()
  913. },
  914.  
  915. next() {
  916. const currentIndex = state.getCurrentLinkIndex()
  917. const newIndex =
  918. currentIndex < state.getLastLinkIndex() ? currentIndex + 1 : 0
  919.  
  920. image.show(state.linksSet[newIndex])
  921. },
  922.  
  923. previous() {
  924. const currentIndex = state.getCurrentLinkIndex()
  925. const newIndex =
  926. currentIndex === 0 ? state.getLastLinkIndex() : currentIndex - 1
  927.  
  928. image.show(state.linksSet[newIndex])
  929. },
  930.  
  931. toggleFullHeight() {
  932. elements.container.classList.toggle(CLASSES.fullHeight)
  933. elements.buttons.toggleFullHeight.classList.toggle(CLASSES.iconExpand)
  934. elements.buttons.toggleFullHeight.classList.toggle(CLASSES.iconShrink)
  935. },
  936.  
  937. markAsBroken(link) {
  938. elements.container.classList.replace(
  939. CLASSES.loadingIcon,
  940. CLASSES.brokenImage
  941. )
  942. elements.container.classList.remove(CLASSES.loading)
  943. link.classList.replace(CLASSES.imageLinkZoom, CLASSES.brokenImage)
  944. },
  945. }
  946.  
  947. const events = {
  948. linkClick(e) {
  949. e.preventDefault()
  950.  
  951. if (state.firstClick) {
  952. create.viewContainer()
  953. state.firstClick = false
  954. }
  955.  
  956. const link = e.target
  957.  
  958. state.linksSet = $$(SELECTORS.imageLink, link.parentNode)
  959. state.isSingle = state.linksSet.length === 1
  960.  
  961. if (!state.isSingle) {
  962. elements.imageTotal.textContent = state.linksSet.length
  963. }
  964.  
  965. events.keyboard.bind()
  966.  
  967. image.show(link)
  968. },
  969.  
  970. keyboard: {
  971. bind() {
  972. document.addEventListener('keydown', events.keyboard.handler, true)
  973. },
  974. unbind() {
  975. document.removeEventListener('keydown', events.keyboard.handler, true)
  976. },
  977. handler(e) {
  978. if (e.defaultPrevented || e.repeat) {
  979. return
  980. }
  981.  
  982. switch (e.key) {
  983. case 'ArrowRight':
  984. image.next()
  985. break
  986.  
  987. case 'ArrowLeft':
  988. image.previous()
  989. break
  990.  
  991. case 'Escape':
  992. image.hide()
  993. break
  994.  
  995. case ' ':
  996. image.toggleFullHeight()
  997. break
  998.  
  999. default:
  1000. return
  1001. }
  1002.  
  1003. e.preventDefault()
  1004. },
  1005. },
  1006.  
  1007. mouse(e) {
  1008. switch (e.type) {
  1009. case 'mousedown':
  1010. state.dragging = true
  1011. state.dragPosition = e.clientY
  1012. elements.image.classList.add(CLASSES.grabbing)
  1013. break
  1014.  
  1015. case 'mousemove':
  1016. if (state.dragging) {
  1017. elements.imageContainer.scrollTop -=
  1018. e.clientY - state.dragPosition
  1019. state.dragPosition = e.clientY
  1020. }
  1021. break
  1022.  
  1023. case 'mouseup':
  1024. case 'mouseout':
  1025. state.dragging = false
  1026. elements.image.classList.remove(CLASSES.grabbing)
  1027. break
  1028.  
  1029. case 'dblclick':
  1030. image.toggleFullHeight()
  1031. break
  1032.  
  1033. default:
  1034. return
  1035. }
  1036.  
  1037. e.preventDefault()
  1038. },
  1039. }
  1040.  
  1041. const create = {
  1042. viewContainer() {
  1043. elements.container = $.create('div', {
  1044. className: 'iv-image-view iv-icon iv-icon--size-button',
  1045. contents: [
  1046. create.viewContainerHeader(),
  1047. create.viewContainerBody(),
  1048. create.viewContainerFooter(),
  1049. ],
  1050. })
  1051.  
  1052. document.body.appendChild(elements.container)
  1053. },
  1054.  
  1055. viewContainerBody() {
  1056. elements.image = $.create('img', {
  1057. className: 'iv-image',
  1058. events: {
  1059. 'mousedown mouseup mousemove mouseout dblclick': events.mouse,
  1060. },
  1061. })
  1062.  
  1063. elements.imageThumbnail = $.create('img', {
  1064. className: 'iv-thumbnail',
  1065. })
  1066.  
  1067. elements.imageContainer = $.create('div', {
  1068. className: 'iv-image-view__body',
  1069. contents: [
  1070. {
  1071. tag: 'div',
  1072. className: 'iv-image-view__backdrop',
  1073. events: {
  1074. click: image.hide,
  1075. },
  1076. },
  1077. {
  1078. tag: 'div',
  1079. className: 'iv-thumbnail-wrapper',
  1080. contents: elements.imageThumbnail,
  1081. },
  1082. elements.image,
  1083. ],
  1084. })
  1085.  
  1086. return elements.imageContainer
  1087. },
  1088.  
  1089. viewContainerHeader() {
  1090. elements.imageNumber = document.createElement('span')
  1091. elements.imageTotal = document.createElement('span')
  1092. const imageNumber = $.create('div', {
  1093. className: 'iv-image-view__number single-hide',
  1094. contents: [elements.imageNumber, '/', elements.imageTotal],
  1095. })
  1096.  
  1097. elements.buttons.close = create.toolbarButton(
  1098. 'Close (Esc)',
  1099. 'close',
  1100. image.hide
  1101. )
  1102.  
  1103. return {
  1104. tag: 'div',
  1105. className: 'iv-image-view__header-wrapper',
  1106. contents: {
  1107. tag: 'div',
  1108. className: 'iv-image-view__header',
  1109. contents: [imageNumber, elements.buttons.close],
  1110. },
  1111. }
  1112. },
  1113.  
  1114. viewContainerFooter() {
  1115. const buttons = elements.buttons
  1116. buttons.previous = create.toolbarButton(
  1117. 'Previous (←)',
  1118. 'previous',
  1119. image.previous,
  1120. 'single-hide'
  1121. )
  1122. buttons.toggleFullHeight = create.toolbarButton(
  1123. 'Toggle full height (Space)',
  1124. 'expand',
  1125. image.toggleFullHeight
  1126. )
  1127. buttons.next = create.toolbarButton(
  1128. 'Next (→)',
  1129. 'next',
  1130. image.next,
  1131. 'single-hide'
  1132. )
  1133.  
  1134. return {
  1135. tag: 'div',
  1136. className: 'iv-image-view__footer-wrapper',
  1137. contents: {
  1138. tag: 'div',
  1139. className: 'iv-image-view__footer',
  1140. contents: [
  1141. buttons.previous,
  1142. buttons.toggleFullHeight,
  1143. buttons.next,
  1144. ],
  1145. },
  1146. }
  1147. },
  1148.  
  1149. toolbarButton(title, icon, handler, className = '') {
  1150. return $.create('a', {
  1151. href: '#',
  1152. title: title,
  1153. className: `iv-icon-button iv-icon iv-icon--type-${icon} ${className}`,
  1154. events: {
  1155. click: (e) => {
  1156. e.preventDefault()
  1157. handler()
  1158. },
  1159. },
  1160. })
  1161. },
  1162. }
  1163.  
  1164. return function (enabledHosts) {
  1165. addStyle(css_248z)
  1166.  
  1167. const container = $('body')
  1168.  
  1169. const linkClasses = [
  1170. CLASSES.imageLink,
  1171. 'iv-image-link',
  1172. 'iv-icon',
  1173. 'iv-icon--hover',
  1174. CLASSES.imageLinkZoom,
  1175. 'iv-icon--size-button',
  1176. ]
  1177.  
  1178. const getHostName = urlExtractor.getHostNameMatcher(enabledHosts)
  1179.  
  1180. const imagesWithLinks = $$('a > img, a > var', container)
  1181.  
  1182. imagesWithLinks
  1183. .map((img) => {
  1184. return { link: img.parentNode, thumbnailUrl: img.src || img.title }
  1185. })
  1186. .filter(({ link }) => link.href)
  1187. .forEach(({ link, thumbnailUrl }) => {
  1188. const hostName = getHostName(link.href)
  1189.  
  1190. if (hostName) {
  1191. link.dataset.ivHost = hostName
  1192. link.dataset.ivThumbnail = thumbnailUrl
  1193. link.classList.add(...linkClasses)
  1194. }
  1195. })
  1196.  
  1197. $.delegate(container, 'click', SELECTORS.imageLink, events.linkClick)
  1198. }
  1199. })()
  1200.  
  1201. $.ready().then(async () => {
  1202. const hostConfig = await config.init()
  1203.  
  1204. initViewer(hostConfig.enabledHosts)
  1205. })
  1206. })()