Image Viewer

Allows viewing full image without leaving the page

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

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