Image Viewer

View full image without leaving the page or on a new tab without ads

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