Image Viewer

View full image without leaving the page

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

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