FA Embedded Image Viewer

Embeds the clicked Image on the Current Site, so you can view it without loading the submission Page

当前为 2024-11-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name FA Embedded Image Viewer
  3. // @namespace Violentmonkey Scripts
  4. // @match *://*.furaffinity.net/*
  5. // @require https://update.greasyfork.org/scripts/475041/1267274/Furaffinity-Custom-Settings.js
  6. // @require https://update.greasyfork.org/scripts/483952/1486330/Furaffinity-Request-Helper.js
  7. // @require https://update.greasyfork.org/scripts/485153/1316289/Furaffinity-Loading-Animations.js
  8. // @require https://update.greasyfork.org/scripts/476762/1318215/Furaffinity-Custom-Pages.js
  9. // @require https://update.greasyfork.org/scripts/485827/1326313/Furaffinity-Match-List.js
  10. // @require https://update.greasyfork.org/scripts/492931/1363921/Furaffinity-Submission-Image-Viewer.js
  11. // @grant GM_info
  12. // @version 2.3.0
  13. // @author Midori Dragon
  14. // @description Embeds the clicked Image on the Current Site, so you can view it without loading the submission Page
  15. // @icon https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png?v2
  16. // @license MIT
  17. // ==/UserScript==
  18. // jshint esversion: 8
  19. (() => {
  20. "use strict";
  21. var __webpack_require__ = {
  22. d: (exports, definition) => {
  23. for (var key in definition) __webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key) && Object.defineProperty(exports, key, {
  24. enumerable: !0,
  25. get: definition[key]
  26. });
  27. },
  28. o: (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop)
  29. };
  30. __webpack_require__.d({}, {
  31. qJ: () => alwaysZoomCenterSetting,
  32. yr: () => closeEmbedAfterOpenSetting,
  33. xe: () => loadingSpinSpeedFavSetting,
  34. _d: () => loadingSpinSpeedSetting,
  35. h_: () => openInNewTabSetting,
  36. e2: () => previewQualitySetting,
  37. uL: () => requestHelper,
  38. t0: () => useCtrlForZoomSetting
  39. });
  40. class EmbeddedCSS {
  41. constructor() {
  42. this.createStyle();
  43. }
  44. createStyle() {
  45. if (document.getElementById("embeddedStyle")) return;
  46. const style = document.createElement("style");
  47. style.id = "embeddedStyle", style.type = "text/css", style.innerHTML = EmbeddedCSS.css,
  48. document.head.appendChild(style);
  49. }
  50. static get css() {
  51. return "\n#embeddedElem {\n position: fixed;\n width: 100vw;\n height: 100vh;\n max-width: 1850px;\n z-index: 999999;\n background: rgba(30,33,38,.65);\n}\n#embeddedBackgroundElem {\n position: fixed;\n display: flex;\n flex-direction: column;\n left: 50%;\n transform: translate(-50%, 0%);\n margin-top: 20px;\n padding: 20px;\n background: rgba(30,33,38,.90);\n border-radius: 10px;\n}\n.embeddedSubmissionImg {\n max-width: inherit;\n max-height: inherit;\n border-radius: 10px;\n user-select: none;\n}\n#embeddedButtonsContainer {\n position: relative;\n margin-top: 20px;\n margin-bottom: 20px;\n margin-left: 20px;\n}\n#embeddedButtonsWrapper {\n display: flex;\n justify-content: center;\n align-items: center;\n}\n#previewLoadingSpinnerContainer {\n position: absolute;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n}\n.embeddedButton {\n margin-left: 4px;\n margin-right: 4px;\n user-select: none;\n}";
  52. }
  53. }
  54. class EmbeddedHTML {
  55. generateHtmlString() {
  56. return EmbeddedHTML.generateHtmlString();
  57. }
  58. static generateHtmlString() {
  59. return '\n<div id="embeddedBackgroundElem">\n <a id="embeddedSubmissionContainer"></a>\n <div id="embeddedButtonsContainer">\n <div id="embeddedButtonsWrapper">\n <a id="embeddedFavButton" type="button" class="embeddedButton button standard mobile-fix">⠀⠀</a>\n <a id="embeddedDownloadButton" type="button" class="embeddedButton button standard mobile-fix">Download</a>\n <a id="embeddedOpenButton" type="button" class="embeddedButton button standard mobile-fix">Open</a>\n <a id="embeddedOpenGalleryButton" type="button" class="embeddedButton button standard mobile-fix" style="display: none;">Open Gallery</a>\n <a id="embeddedCloseButton" type="button" class="embeddedButton button standard mobile-fix">Close</a>\n </div>\n <div id="previewLoadingSpinnerContainer"></div>\n </div>\n</div>';
  60. }
  61. }
  62. class EmbeddedImage {
  63. constructor(figure) {
  64. this._imageLoaded = !1, this.embeddedElem, this.submissionImg, this.favRequestRunning = !1,
  65. this.downloadRequestRunning = !1, this._onRemoveAction, this.createElements(figure);
  66. const submissionContainer = document.getElementById("embeddedSubmissionContainer"), previewLoadingSpinnerContainer = document.getElementById("previewLoadingSpinnerContainer");
  67. this.loadingSpinner = new LoadingSpinner(submissionContainer), this.loadingSpinner.delay = loadingSpinSpeedSetting.value,
  68. this.loadingSpinner.spinnerThickness = 6, this.loadingSpinner.visible = !0, this.previewLoadingSpinner = new LoadingSpinner(previewLoadingSpinnerContainer),
  69. this.previewLoadingSpinner.delay = loadingSpinSpeedSetting.value, this.previewLoadingSpinner.spinnerThickness = 4,
  70. this.previewLoadingSpinner.size = 40, this._onDocumentClick = this._onDocumentClick.bind(this),
  71. document.addEventListener("click", this._onDocumentClick), this.fillSubDocInfos(figure);
  72. }
  73. static get embeddedExists() {
  74. return !!document.getElementById("embeddedElem");
  75. }
  76. onRemove(action) {
  77. this._onRemoveAction = action;
  78. }
  79. remove() {
  80. this.embeddedElem.parentNode.removeChild(this.embeddedElem), document.removeEventListener("click", this._onDocumentClick),
  81. this._onRemoveAction && this._onRemoveAction();
  82. }
  83. _onDocumentClick(event) {
  84. event.target === document.documentElement && this.remove();
  85. }
  86. createElements(figure) {
  87. this.embeddedElem = document.createElement("div"), this.embeddedElem.id = "embeddedElem",
  88. this.embeddedElem.innerHTML = EmbeddedHTML.generateHtmlString();
  89. document.getElementById("ddmenu").appendChild(this.embeddedElem), this.embeddedElem.addEventListener("click", (event => {
  90. event.target == this.embeddedElem && this.remove();
  91. }));
  92. const zoomLevels = new WeakMap, backgroundElem = document.getElementById("embeddedBackgroundElem");
  93. backgroundElem.addEventListener("wheel", (event => {
  94. if (!0 === useCtrlForZoomSetting.value && !event.ctrlKey) return;
  95. event.preventDefault(), zoomLevels.has(backgroundElem) || zoomLevels.set(backgroundElem, 1);
  96. let zoomLevel = zoomLevels.get(backgroundElem);
  97. if (zoomLevel = event.deltaY < 0 ? zoomLevel + .1 : Math.max(.1, zoomLevel - .1),
  98. zoomLevels.set(backgroundElem, zoomLevel), !0 === alwaysZoomCenterSetting.value) {
  99. const rect = backgroundElem.getBoundingClientRect(), mouseX = (event.clientX - rect.left) / rect.width * 100, mouseY = (event.clientY - rect.top) / rect.height * 100;
  100. backgroundElem.style.transformOrigin = `${mouseX}% ${mouseY}%`;
  101. } else backgroundElem.style.transformOrigin = "center";
  102. const translateMatch = (backgroundElem.style.transform || "").match(/translate\([^)]+\)/), translateValue = translateMatch ? translateMatch[0] : "translate(-50%, 0%)";
  103. backgroundElem.style.transform = `${translateValue} scale(${zoomLevel})`;
  104. }));
  105. const submissionContainer = document.getElementById("embeddedSubmissionContainer");
  106. !0 === openInNewTabSetting.value && submissionContainer.setAttribute("target", "_blank"),
  107. submissionContainer.addEventListener("click", (() => {
  108. !0 === closeEmbedAfterOpenSetting.value && this.remove();
  109. }));
  110. const userLink = function(figcaption) {
  111. if (figcaption) {
  112. const infos = figcaption.querySelectorAll("i");
  113. let userLink;
  114. for (const info of Array.from(infos)) if (info.textContent.toLowerCase().includes("by")) {
  115. const linkElem = info.parentNode.querySelector("a[href][title]");
  116. linkElem && (userLink = linkElem.getAttribute("href"));
  117. }
  118. return userLink;
  119. }
  120. }(figure.querySelector("figcaption"));
  121. if (userLink) {
  122. const galleryLink = trimEnd(userLink, "/").replace("user", "gallery"), scrapsLink = trimEnd(userLink, "/").replace("user", "scraps");
  123. if (!window.location.toString().includes(userLink) && !window.location.toString().includes(galleryLink) && !window.location.toString().includes(scrapsLink)) {
  124. const openGalleryButton = document.getElementById("embeddedOpenGalleryButton");
  125. openGalleryButton.style.display = "block", openGalleryButton.setAttribute("href", galleryLink),
  126. !0 === openInNewTabSetting.value && openGalleryButton.setAttribute("target", "_blank"),
  127. openGalleryButton.onclick = () => {
  128. !0 === closeEmbedAfterOpenSetting.value && this.remove();
  129. };
  130. }
  131. }
  132. const link = figure.querySelector("a[href]").getAttribute("href"), openButton = document.getElementById("embeddedOpenButton");
  133. openButton.setAttribute("href", link), !0 === openInNewTabSetting.value && openButton.setAttribute("target", "_blank"),
  134. openButton.onclick = () => {
  135. !0 === closeEmbedAfterOpenSetting.value && this.remove();
  136. };
  137. document.getElementById("embeddedCloseButton").onclick = () => this.remove();
  138. document.getElementById("previewLoadingSpinnerContainer").onclick = () => {
  139. this.previewLoadingSpinner.visible = !1;
  140. };
  141. }
  142. async fillSubDocInfos(figure) {
  143. const sid = figure.id.split("-")[1], ddmenu = document.getElementById("ddmenu"), doc = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
  144. if (doc) {
  145. this.submissionImg = doc.getElementById("submissionImg");
  146. const imgSrc = this.submissionImg.src;
  147. let prevSrc = this.submissionImg.getAttribute("data-preview-src");
  148. previewQualitySetting.value <= 2 ? prevSrc = prevSrc.replace("@600", "@200") : 3 === previewQualitySetting.value ? prevSrc = prevSrc.replace("@600", "@300") : 4 === previewQualitySetting.value && (prevSrc = prevSrc.replace("@600", "@400"));
  149. const faImageViewer = new CustomImageViewer(imgSrc, prevSrc);
  150. faImageViewer.faImage.id = "embeddedSubmissionImg", faImageViewer.faImagePreview.id = "previewSubmissionImg",
  151. faImageViewer.faImage.className = faImageViewer.faImagePreview.className = "embeddedSubmissionImg",
  152. faImageViewer.faImage.style.maxWidth = faImageViewer.faImagePreview.style.maxWidth = window.innerWidth - 40 + "px",
  153. faImageViewer.faImage.style.maxHeight = faImageViewer.faImagePreview.style.maxHeight = window.innerHeight - ddmenu.clientHeight - 76 - 40 - 100 + "px",
  154. faImageViewer.onImageLoadStart = () => {
  155. this._imageLoaded = !1, this.loadingSpinner && (this.loadingSpinner.visible = !1);
  156. }, faImageViewer.onImageLoad = () => {
  157. this._imageLoaded = !0, this.loadingSpinner && !0 === this.loadingSpinner.visible && (this.loadingSpinner.visible = !1),
  158. this.previewLoadingSpinner && !0 === this.previewLoadingSpinner.visible && (this.previewLoadingSpinner.visible = !1);
  159. }, faImageViewer.onPreviewImageLoad = () => {
  160. !1 === this._imageLoaded && (this.previewLoadingSpinner.visible = !0);
  161. };
  162. const submissionContainer = document.getElementById("embeddedSubmissionContainer");
  163. faImageViewer.load(submissionContainer);
  164. const url = doc.querySelector('meta[property="og:url"]').content;
  165. submissionContainer.setAttribute("href", url);
  166. const result = function(doc) {
  167. const columnPage = doc.getElementById("columnpage"), buttons = columnPage.querySelector('div[class*="favorite-nav"').querySelectorAll('a[class*="button"][href]');
  168. let favButton;
  169. for (const button of Array.from(buttons)) button.textContent.toLowerCase().includes("fav") && (favButton = button);
  170. if (favButton) {
  171. return {
  172. favKey: favButton.getAttribute("href").split("?key=")[1],
  173. isFav: !favButton.getAttribute("href").toLowerCase().includes("unfav")
  174. };
  175. }
  176. return null;
  177. }(doc), favButton = document.getElementById("embeddedFavButton");
  178. favButton.textContent = result.isFav ? "+Fav" : "-Fav", favButton.setAttribute("isFav", result.isFav),
  179. favButton.setAttribute("key", result.favKey), favButton.addEventListener("click", (() => {
  180. !1 === this.favRequestRunning && this.doFavRequest(sid);
  181. }));
  182. const downloadButton = document.getElementById("embeddedDownloadButton");
  183. downloadButton.addEventListener("click", (() => {
  184. if (!0 === this.downloadRequestRunning) return;
  185. this.downloadRequestRunning = !0;
  186. const loadingTextSpinner = new LoadingTextSpinner(downloadButton);
  187. loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value, loadingTextSpinner.visible = !0;
  188. const iframe = document.createElement("iframe");
  189. iframe.style.display = "none", iframe.src = this.submissionImg.src + "?eidownload",
  190. iframe.addEventListener("load", (() => {
  191. this.downloadRequestRunning = !1, loadingTextSpinner.visible = !1, setTimeout((() => iframe.parentNode.removeChild(iframe)), 100);
  192. })), document.body.appendChild(iframe);
  193. }));
  194. }
  195. }
  196. async doFavRequest(sid) {
  197. const favButton = document.getElementById("embeddedFavButton");
  198. this.favRequestRunning = !0;
  199. const loadingTextSpinner = new LoadingTextSpinner(favButton);
  200. loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value, loadingTextSpinner.visible = !0;
  201. let favKey = favButton.getAttribute("key"), isFav = "true" == favButton.getAttribute("isFav");
  202. !0 === isFav ? (favKey = await requestHelper.SubmissionRequests.favSubmission(sid, favKey),
  203. loadingTextSpinner.visible = !1, favKey ? (favButton.setAttribute("key", favKey),
  204. isFav = !1, favButton.setAttribute("isFav", isFav.toString()), favButton.textContent = "-Fav") : (favButton.textContent = "x",
  205. setTimeout((() => favButton.textContent = "+Fav"), 1e3))) : (favKey = await requestHelper.SubmissionRequests.unfavSubmission(sid, favKey),
  206. loadingTextSpinner.visible = !1, favKey ? (favButton.setAttribute("key", favKey),
  207. isFav = !0, favButton.setAttribute("isFav", isFav.toString()), favButton.textContent = "+Fav") : (favButton.textContent = "x",
  208. setTimeout((() => favButton.textContent = "-Fav"), 1e3))), this.favRequestRunning = !1;
  209. }
  210. }
  211. async function addEmbedded() {
  212. const nonEmbeddedFigures = document.querySelectorAll("figure:not([embedded])");
  213. for (const figure of Array.from(nonEmbeddedFigures)) figure.setAttribute("embedded", "true"),
  214. figure.addEventListener("click", (event => {
  215. if (event instanceof MouseEvent && event.target instanceof HTMLElement && !event.ctrlKey && !event.target.id.includes("favbutton") && "checkbox" !== event.target.getAttribute("type")) {
  216. if (event.target.getAttribute("href")) return;
  217. event.preventDefault(), !EmbeddedImage.embeddedExists && figure instanceof HTMLElement && new EmbeddedImage(figure);
  218. }
  219. }));
  220. }
  221. function trimEnd(string, toRemove) {
  222. return string.endsWith(toRemove) && (string = string.slice(0, -1)), string;
  223. }
  224. CustomSettings.name = "Extension Settings", CustomSettings.provider = "Midori's Script Settings",
  225. CustomSettings.headerName = `${GM_info.script.name} Settings`;
  226. const openInNewTabSetting = CustomSettings.newSetting("Open in new Tab", "Wether to open links in a new Tab or the current one.", SettingTypes.Boolean, "Open in new Tab", !0), loadingSpinSpeedFavSetting = CustomSettings.newSetting("Fav Loading Animation", "The duration that the loading animation, for faving a submission, takes for a full rotation in milliseconds.", SettingTypes.Number, "", 600), loadingSpinSpeedSetting = CustomSettings.newSetting("Embedded Loading Animation", "The duration that the loading animation of the Embedded element to load takes for a full rotation in milliseconds.", SettingTypes.Number, "", 1e3), closeEmbedAfterOpenSetting = CustomSettings.newSetting("Close Embed after open", "Wether to clos the current embedded Submission after it is opened in a new Tab (also for open Gallery).", SettingTypes.Boolean, "Close Embed after open", !0), useCtrlForZoomSetting = CustomSettings.newSetting("Use Ctrl for Zoom", "Wether the Ctrl-Key needs to be pressed while scrolling to zoom the Embedded Image.", SettingTypes.Boolean, "Use Ctrl for Zoom", !1), alwaysZoomCenterSetting = CustomSettings.newSetting("Zoom from Mouse Location", "Wether the Embedded Image should be zoomed from the Mouse Location. (Otherwise from Center)", SettingTypes.Boolean, "Zoom from Mouse Location", !0), previewQualitySetting = CustomSettings.newSetting("Preview Quality", "The quality of the preview image. Value range is 2-6. (Higher values can be slower)", SettingTypes.Number, "", 3);
  227. CustomSettings.loadSettings();
  228. const requestHelper = new FARequestHelper(2), matchList = new MatchList(CustomSettings);
  229. if (matchList.matches = [ "net/browse", "net/user", "net/gallery", "net/search", "net/favorites", "net/scraps", "net/controls/favorites", "net/controls/submissions", "net/msg/submissions", "d.furaffinity.net" ],
  230. matchList.runInIFrame = !0, matchList.hasMatch()) {
  231. const page = new CustomPage("d.furaffinity.net", "eidownload");
  232. let pageDownload = !1;
  233. page.onopen = () => {
  234. !function() {
  235. let url = window.location.toString();
  236. if (url.includes("?")) {
  237. const parts = url.split("?");
  238. url = parts[0];
  239. }
  240. const download = document.createElement("a");
  241. download.href = url, download.download = url.substring(url.lastIndexOf("/") + 1),
  242. download.style.display = "none", document.body.appendChild(download), download.click(),
  243. document.body.removeChild(download), window.close();
  244. }(), pageDownload = !0;
  245. }, !1 === pageDownload && !1 === matchList.isWindowIFrame() && (new EmbeddedCSS,
  246. addEmbedded(), window.addEventListener("updateEmbeddedEvent", (async () => {
  247. await addEmbedded();
  248. })));
  249. }
  250. })();