GitHub Image Previewer

Previews various image formats, including JPG, PNG, GIF, BMP, TIFF, WebP, SVG, and ICO.

  1. // ==UserScript==
  2. // @name GitHub Image Previewer
  3. // @description Previews various image formats, including JPG, PNG, GIF, BMP, TIFF, WebP, SVG, and ICO.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.2
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant none
  12. // @run-at document-end
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const GitHubImagePreviewer = {
  19. supportedImageFormats: /\.(jpe?g|png|gif|bmp|ico|tiff?|webp|svg)$/i,
  20. imagePreviewContainer: null,
  21. imagePreviewElement: null,
  22. imagePreviewTitle: null,
  23. imagePreviewMetadata: null,
  24.  
  25. init() {
  26. this.createImagePreviewElements();
  27. this.attachImagePreviewListeners();
  28. this.disableGitHubTooltips();
  29. },
  30.  
  31. createImagePreviewElements() {
  32. this.imagePreviewContainer = document.createElement('div');
  33. this.imagePreviewContainer.style.cssText = `
  34. position: fixed;
  35. z-index: 9999;
  36. background: white;
  37. border: 1px solid #d1d5da;
  38. border-radius: 3px;
  39. box-shadow: 0 1px 5px rgba(27,31,35,0.15);
  40. display: none;
  41. padding: 10px;
  42. max-width: 300px;
  43. max-height: 300px;
  44. color: #24292e;
  45. `;
  46.  
  47. this.imagePreviewTitle = document.createElement('div');
  48. this.imagePreviewTitle.style.cssText = `
  49. font-weight: bold;
  50. margin-bottom: 5px;
  51. white-space: nowrap;
  52. overflow: hidden;
  53. text-overflow: ellipsis;
  54. `;
  55.  
  56. this.imagePreviewElement = document.createElement('img');
  57. this.imagePreviewElement.style.cssText = `
  58. max-width: 100%;
  59. max-height: 200px;
  60. display: block;
  61. margin: 0 auto;
  62. background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
  63. linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
  64. linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
  65. linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
  66. background-size: 20px 20px;
  67. background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
  68. `;
  69.  
  70. this.imagePreviewMetadata = document.createElement('div');
  71. this.imagePreviewMetadata.style.cssText = `
  72. font-size: 12px;
  73. color: #586069;
  74. margin-top: 5px;
  75. text-align: center;
  76. `;
  77.  
  78. this.imagePreviewContainer.appendChild(this.imagePreviewTitle);
  79. this.imagePreviewContainer.appendChild(this.imagePreviewElement);
  80. this.imagePreviewContainer.appendChild(this.imagePreviewMetadata);
  81. document.body.appendChild(this.imagePreviewContainer);
  82. },
  83.  
  84. attachImagePreviewListeners() {
  85. document.addEventListener('mouseover', this.handleImageMouseOver.bind(this));
  86. document.addEventListener('mouseout', this.handleImageMouseOut.bind(this));
  87. document.addEventListener('mousemove', this.handleImageMouseMove.bind(this));
  88. },
  89.  
  90. disableGitHubTooltips() {
  91. const style = document.createElement('style');
  92. style.textContent = `
  93. .tooltipped::before, .tooltipped::after {
  94. display: none !important;
  95. }
  96. `;
  97. document.head.appendChild(style);
  98. },
  99.  
  100. handleImageMouseOver(event) {
  101. const target = event.target.closest('a');
  102. if (target && this.supportedImageFormats.test(target.href)) {
  103. if (target.hasAttribute('title')) {
  104. target.dataset.originalTitle = target.getAttribute('title');
  105. target.removeAttribute('title');
  106. }
  107. this.showImagePreview(target);
  108. }
  109. },
  110.  
  111. handleImageMouseOut() {
  112. this.hideImagePreview();
  113. },
  114.  
  115. handleImageMouseMove(event) {
  116. if (this.imagePreviewContainer.style.display !== 'none') {
  117. const mouseX = event.clientX;
  118. const mouseY = event.clientY;
  119. const viewportWidth = window.innerWidth;
  120. const viewportHeight = window.innerHeight;
  121. const previewWidth = this.imagePreviewContainer.offsetWidth;
  122. const previewHeight = this.imagePreviewContainer.offsetHeight;
  123. let x = mouseX + 15;
  124. let y = mouseY + 15;
  125. if (x + previewWidth > viewportWidth) {
  126. x = mouseX - previewWidth - 15;
  127. }
  128. if (y + previewHeight > viewportHeight) {
  129. y = mouseY - previewHeight - 15;
  130. }
  131. this.imagePreviewContainer.style.left = `${x}px`;
  132. this.imagePreviewContainer.style.top = `${y}px`;
  133. }
  134. },
  135.  
  136. showImagePreview(target) {
  137. const imageUrl = target.href.replace('/blob/', '/raw/');
  138. this.imagePreviewTitle.textContent = target.dataset.originalTitle || target.getAttribute('title') || target.textContent;
  139. this.imagePreviewElement.src = imageUrl;
  140. this.imagePreviewContainer.style.display = 'block';
  141.  
  142. this.imagePreviewElement.onload = this.updateImageMetadata.bind(this);
  143. },
  144.  
  145. hideImagePreview() {
  146. this.imagePreviewContainer.style.display = 'none';
  147. },
  148.  
  149. updateImageMetadata() {
  150. const width = this.imagePreviewElement.naturalWidth;
  151. const height = this.imagePreviewElement.naturalHeight;
  152. this.imagePreviewMetadata.textContent = `${width} x ${height} px`;
  153. },
  154. };
  155.  
  156. GitHubImagePreviewer.init();
  157. })();