Image Viewer

View full image without leaving the page

当前为 2022-05-14 提交的版本,查看 最新版本

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