FA Embedded Image Viewer

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

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

  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/1329447/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.2.1
  13. // @author Midori Dragon
  14. // @description Embedds 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. // @homepageURL https://greasyfork.org/de/scripts/458971-embedded-image-viewer
  17. // @supportURL https://greasyfork.org/de/scripts/458971-embedded-image-viewer/feedback
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. // jshint esversion: 8
  22.  
  23. CustomSettings.name = "Extension Settings";
  24. CustomSettings.provider = "Midori's Script Settings";
  25. CustomSettings.headerName = `${GM_info.script.name} Settings`;
  26. 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", true);
  27. const 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);
  28. const 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, "", 1000);
  29. const 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", true);
  30. const 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", false);
  31. const 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", true);
  32. CustomSettings.loadSettings();
  33.  
  34. const matchList = new MatchList(CustomSettings);
  35. 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'];
  36. matchList.runInIFrame = true;
  37. if (!matchList.hasMatch())
  38. return;
  39.  
  40. const page = new CustomPage("d.furaffinity.net", "eidownload");
  41. page.onopen = (data) => {
  42. downloadImage();
  43. return;
  44. };
  45.  
  46. if (matchList.isWindowIFrame() == true)
  47. return;
  48.  
  49. const requestHelper = new FARequestHelper(2);
  50.  
  51. class EmbeddedImage {
  52. constructor(figure) {
  53. this._previewLoaded;
  54. this._imageLoaded;
  55.  
  56. this.embeddedElem;
  57. this.backgroundElem;
  58. this.submissionContainer;
  59. this.submissionImg;
  60. this.buttonsContainer;
  61. this.previewLoadingSpinnerContainer;
  62. this.favButton;
  63. this.downloadButton;
  64. this.closeButton;
  65.  
  66. this.favRequestRunning = false;
  67. this.downloadRequestRunning = false;
  68.  
  69. this._onRemoveAction;
  70.  
  71. this.createStyle();
  72. this.createElements(figure);
  73.  
  74. this.loadingSpinner = new LoadingSpinner(this.submissionContainer);
  75. this.loadingSpinner.delay = loadingSpinSpeedSetting.value;
  76. this.loadingSpinner.spinnerThickness = 6;
  77. this.loadingSpinner.visible = true;
  78.  
  79. this.previewLoadingSpinner = new LoadingSpinner(this.previewLoadingSpinnerContainer);
  80. this.previewLoadingSpinner.delay = loadingSpinSpeedSetting.value;
  81. this.previewLoadingSpinner.spinnerThickness = 4;
  82. this.previewLoadingSpinner.size = 40;
  83.  
  84. this.fillSubDocInfos(figure);
  85. }
  86.  
  87. createStyle() {
  88. if (document.getElementById("embeddedStyle")) return;
  89. const style = document.createElement("style");
  90. style.id = "embeddedStyle";
  91. style.type = "text/css";
  92. style.innerHTML = `
  93. #embeddedElem {
  94. position: fixed;
  95. width: 100vw;
  96. height: 100vh;
  97. max-width: 1850px;
  98. z-index: 999999;
  99. background: rgba(30,33,38,.65);
  100. }
  101. #embeddedBackgroundElem {
  102. position: fixed;
  103. display: flex;
  104. flex-direction: column;
  105. left: 50%;
  106. transform: translate(-50%, 0%);
  107. margin-top: 20px;
  108. padding: 20px;
  109. background: rgba(30,33,38,.90);
  110. border-radius: 10px;
  111. }
  112. .embeddedSubmissionImg {
  113. max-width: inherit;
  114. max-height: inherit;
  115. border-radius: 10px;
  116. user-select: none;
  117. }
  118. #embeddedButtonsContainer {
  119. position: relative;
  120. margin-top: 20px;
  121. margin-bottom: 20px;
  122. margin-left: 20px;
  123. }
  124. #embeddedButtonsWrapper {
  125. display: flex;
  126. justify-content: center;
  127. align-items: center;
  128. }
  129. #previewLoadingSpinnerContainer {
  130. position: absolute;
  131. top: 50%;
  132. right: 0;
  133. transform: translateY(-50%);
  134. }
  135. .embeddedButton {
  136. margin-left: 4px;
  137. margin-right: 4px;
  138. user-select: none;
  139. }
  140. `;
  141. document.head.appendChild(style);
  142. }
  143.  
  144. onRemove(action) {
  145. this._onRemoveAction = action;
  146. }
  147.  
  148. remove() {
  149. this.embeddedElem.parentNode.removeChild(this.embeddedElem);
  150. if (this._onRemoveAction)
  151. this._onRemoveAction();
  152. }
  153.  
  154. createElements(figure) {
  155. this.embeddedElem = document.createElement("div");
  156. this.embeddedElem.id = "embeddedElem";
  157. this.embeddedElem.onclick = (event) => {
  158. if (event.target == this.embeddedElem)
  159. this.remove();
  160. };
  161.  
  162. const zoomLevels = new WeakMap();
  163. this.backgroundElem = document.createElement("div");
  164. this.backgroundElem.id = "embeddedBackgroundElem";
  165. this.backgroundElem.addEventListener('wheel', (event) => {
  166. if (useCtrlForZoomSetting.value === true && !event.ctrlKey) {
  167. return;
  168. }
  169. event.preventDefault(); // Prevent page scroll
  170.  
  171. // Initialize zoom level for this image if not already set
  172. if (!zoomLevels.has(this.backgroundElem)) {
  173. zoomLevels.set(this.backgroundElem, 1);
  174. }
  175.  
  176. // Get the current zoom level
  177. let zoomLevel = zoomLevels.get(this.backgroundElem);
  178.  
  179. // Adjust zoom level based on scroll direction
  180. if (event.deltaY < 0) {
  181. zoomLevel += 0.1; // Zoom in
  182. } else {
  183. zoomLevel = Math.max(0.1, zoomLevel - 0.1); // Zoom out, with a minimum limit
  184. }
  185.  
  186. // Save the updated zoom level
  187. zoomLevels.set(this.backgroundElem, zoomLevel);
  188.  
  189. // Calculate mouse position relative to the image
  190. if (alwaysZoomCenterSetting.value === true) {
  191. const rect = this.backgroundElem.getBoundingClientRect();
  192. const mouseX = ((event.clientX - rect.left) / rect.width) * 100;
  193. const mouseY = ((event.clientY - rect.top) / rect.height) * 100;
  194. this.backgroundElem.style.transformOrigin = `${mouseX}% ${mouseY}%`;
  195. } else {
  196. this.backgroundElem.style.transformOrigin = `center`;
  197. }
  198.  
  199. // Get the current transform value
  200. const existingTransform = this.backgroundElem.style.transform || '';
  201.  
  202. // Extract any existing translate transform
  203. const translateMatch = existingTransform.match(/translate\([^)]+\)/);
  204. const translateValue = translateMatch ? translateMatch[0] : 'translate(-50%, 0%)';
  205.  
  206. // Apply the combined transform with scale
  207. this.backgroundElem.style.transform = `${translateValue} scale(${zoomLevel})`;
  208. });
  209. notClosingElemsArr.push(this.backgroundElem.id);
  210.  
  211. this.submissionContainer = document.createElement("a");
  212. this.submissionContainer.id = "embeddedSubmissionContainer";
  213. if (openInNewTabSetting.value == true)
  214. this.submissionContainer.target = "_blank";
  215. this.submissionContainer.onclick = () => {
  216. if (closeEmbedAfterOpenSetting.value == true)
  217. this.remove();
  218. };
  219. notClosingElemsArr.push(this.submissionContainer.id);
  220.  
  221. this.backgroundElem.appendChild(this.submissionContainer);
  222.  
  223. this.buttonsContainer = document.createElement("div");
  224. this.buttonsContainer.id = "embeddedButtonsContainer";
  225. notClosingElemsArr.push(this.buttonsContainer.id);
  226.  
  227. this.buttonsWrapper = document.createElement("div");
  228. this.buttonsWrapper.id = "embeddedButtonsWrapper";
  229. notClosingElemsArr.push(this.buttonsWrapper.id);
  230. this.buttonsContainer.appendChild(this.buttonsWrapper);
  231.  
  232. this.favButton = document.createElement("a");
  233. this.favButton.id = "embeddedFavButton";
  234. notClosingElemsArr.push(this.favButton.id);
  235. this.favButton.type = "button";
  236. this.favButton.className = "embeddedButton button standard mobile-fix";
  237. this.favButton.textContent = "⠀⠀";
  238. this.buttonsWrapper.appendChild(this.favButton);
  239.  
  240. this.downloadButton = document.createElement("a");
  241. this.downloadButton.id = "embeddedDownloadButton";
  242. notClosingElemsArr.push(this.downloadButton.id);
  243. this.downloadButton.type = "button";
  244. this.downloadButton.className = "embeddedButton button standard mobile-fix";
  245. this.downloadButton.textContent = "Download";
  246. this.buttonsWrapper.appendChild(this.downloadButton);
  247.  
  248. const userLink = getByLinkFromFigcaption(figure.querySelector("figcaption"));
  249. if (userLink) {
  250. const galleryLink = trimEnd(userLink, "/").replace("user", "gallery");
  251. const scrapsLink = trimEnd(userLink, "/").replace("user", "scraps");
  252. if (!window.location.toString().includes(userLink) && !window.location.toString().includes(galleryLink) && !window.location.toString().includes(scrapsLink)) {
  253. this.openGalleryButton = document.createElement("a");
  254. this.openGalleryButton.id = "embeddedOpenGalleryButton";
  255. notClosingElemsArr.push(this.openGalleryButton.id);
  256. this.openGalleryButton.type = "button";
  257. this.openGalleryButton.className = "embeddedButton button standard mobile-fix";
  258. this.openGalleryButton.textContent = "Open Gallery";
  259. this.openGalleryButton.href = galleryLink;
  260. if (openInNewTabSetting.value == true)
  261. this.openGalleryButton.target = "_blank";
  262. this.openGalleryButton.onclick = () => {
  263. if (closeEmbedAfterOpenSetting.value == true)
  264. this.remove();
  265. };
  266. this.buttonsWrapper.appendChild(this.openGalleryButton);
  267. }
  268. }
  269.  
  270. this.openButton = document.createElement("a");
  271. this.openButton.id = "embeddedOpenButton";
  272. notClosingElemsArr.push(this.openButton.id);
  273. this.openButton.type = "button";
  274. this.openButton.className = "embeddedButton button standard mobile-fix";
  275. this.openButton.textContent = "Open";
  276. const link = figure.querySelector("a[href]");
  277. this.openButton.href = link;
  278. if (openInNewTabSetting.value == true)
  279. this.openButton.target = "_blank";
  280. this.openButton.onclick = () => {
  281. if (closeEmbedAfterOpenSetting.value == true)
  282. this.remove();
  283. };
  284. this.buttonsWrapper.appendChild(this.openButton);
  285.  
  286. this.closeButton = document.createElement("a");
  287. this.closeButton.id = "embeddedCloseButton";
  288. notClosingElemsArr.push(this.closeButton.id);
  289. this.closeButton.type = "button";
  290. this.closeButton.className = "embeddedButton button standard mobile-fix";
  291. this.closeButton.textContent = "Close";
  292. this.closeButton.onclick = () => this.remove();
  293. this.buttonsWrapper.appendChild(this.closeButton);
  294.  
  295. this.previewLoadingSpinnerContainer = document.createElement("div");
  296. this.previewLoadingSpinnerContainer.id = "previewLoadingSpinnerContainer";
  297. notClosingElemsArr.push(this.previewLoadingSpinnerContainer.id);
  298. this.previewLoadingSpinnerContainer.onclick = () => {
  299. this.previewLoadingSpinner.visible = false;
  300. };
  301. this.buttonsContainer.appendChild(this.previewLoadingSpinnerContainer);
  302.  
  303. this.backgroundElem.appendChild(this.buttonsContainer);
  304.  
  305. this.embeddedElem.appendChild(this.backgroundElem);
  306.  
  307. const ddmenu = document.getElementById("ddmenu");
  308. ddmenu.appendChild(this.embeddedElem);
  309. }
  310.  
  311. async fillSubDocInfos(figure) {
  312. const sid = figure.id.split("-")[1];
  313. const ddmenu = document.getElementById("ddmenu");
  314. const doc = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
  315. if (doc) {
  316. this.submissionImg = doc.getElementById("submissionImg");
  317. const imgSrc = this.submissionImg.src;
  318. const prevSrc = this.submissionImg.getAttribute("data-preview-src");
  319. const prevPrevSrc = prevSrc.replace("@600", "@300");
  320.  
  321. const faImageViewer = new CustomImageViewer(imgSrc, prevSrc);
  322. faImageViewer.faImage.id = "embeddedSubmissionImg";
  323. faImageViewer.faImagePreview.id = "previewSubmissionImg";
  324. faImageViewer.faImage.className = faImageViewer.faImagePreview.className = "embeddedSubmissionImg";
  325. faImageViewer.faImage.style.maxWidth = faImageViewer.faImagePreview.style.maxWidth = window.innerWidth - 20 * 2 + "px";
  326. faImageViewer.faImage.style.maxHeight = faImageViewer.faImagePreview.style.maxHeight = window.innerHeight - ddmenu.clientHeight - 38 * 2 - 20 * 2 - 100 + "px";
  327. faImageViewer.onImageLoadStart = () => {
  328. this._previewLoaded = false;
  329. this._imageLoaded = false;
  330. if (this.loadingSpinner)
  331. this.loadingSpinner.visible = false;
  332. };
  333. faImageViewer.onImageLoad = () => {
  334. this._imageLoaded = true;
  335. if (this.loadingSpinner && this.loadingSpinner.visible === true)
  336. this.loadingSpinner.visible = false;
  337. if (this.previewLoadingSpinner && this.previewLoadingSpinner.visible === true)
  338. this.previewLoadingSpinner.visible = false;
  339. };
  340. faImageViewer.onPreviewImageLoad = () => {
  341. this._previewLoaded = true;
  342. if (this._imageLoaded === false)
  343. this.previewLoadingSpinner.visible = true;
  344. };
  345. faImageViewer.load(this.submissionContainer);
  346.  
  347. this.submissionContainer.href = doc.querySelector('meta[property="og:url"]').content;
  348.  
  349. const result = getFavKey(doc);
  350. this.favButton.textContent = result.isFav ? "+Fav" : "-Fav";
  351. this.favButton.setAttribute("isFav", result.isFav);
  352. this.favButton.setAttribute("key", result.favKey);
  353. this.favButton.onclick = () => {
  354. if (this.favRequestRunning == false)
  355. this.doFavRequest(sid);
  356. };
  357.  
  358. this.downloadButton.onclick = () => {
  359. if (this.downloadRequestRunning == true)
  360. return;
  361. this.downloadRequestRunning = true;
  362. const loadingTextSpinner = new LoadingTextSpinner(this.downloadButton);
  363. loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value;
  364. loadingTextSpinner.visible = true;
  365. const iframe = document.createElement("iframe");
  366. iframe.style.display = "none";
  367. iframe.src = this.submissionImg.src + "?eidownload";
  368. iframe.onload = () => {
  369. this.downloadRequestRunning = false;
  370. loadingTextSpinner.visible = false;
  371. setTimeout(() => iframe.parentNode.removeChild(iframe), 100);
  372. };
  373. document.body.appendChild(iframe);
  374. };
  375. }
  376. }
  377.  
  378. async doFavRequest(sid) {
  379. this.favRequestRunning = true;
  380. const loadingTextSpinner = new LoadingTextSpinner(this.favButton);
  381. loadingTextSpinner.delay = loadingSpinSpeedFavSetting.value;
  382. loadingTextSpinner.visible = true;
  383. let favKey = this.favButton.getAttribute("key");
  384. let isFav = this.favButton.getAttribute("isFav");
  385. if (isFav == "true") {
  386. favKey = await requestHelper.SubmissionRequests.favSubmission(sid, favKey);
  387. loadingTextSpinner.visible = false;
  388. if (favKey) {
  389. this.favButton.setAttribute("key", favKey);
  390. isFav = false;
  391. this.favButton.setAttribute("isFav", isFav);
  392. this.favButton.textContent = "-Fav";
  393. } else {
  394. this.favButton.textContent = "x";
  395. setTimeout(() => this.favButton.textContent = "+Fav", 1000);
  396. }
  397. } else {
  398. favKey = await requestHelper.SubmissionRequests.unfavSubmission(sid, favKey);
  399. loadingTextSpinner.visible = false;
  400. if (favKey) {
  401. this.favButton.setAttribute("key", favKey);
  402. isFav = true;
  403. this.favButton.setAttribute("isFav", isFav);
  404. this.favButton.textContent = "+Fav";
  405. } else {
  406. this.favButton.textContent = "x";
  407. setTimeout(() => this.favButton.textContent = "-Fav", 1000);
  408. }
  409. }
  410. this.favRequestRunning = false;
  411. }
  412. }
  413.  
  414. function getByLinkFromFigcaption(figcaption) {
  415. if (figcaption) {
  416. const infos = figcaption.querySelectorAll("i");
  417. let userLink;
  418. for (const info of infos) {
  419. if (info.textContent.toLowerCase().includes("by")) {
  420. const linkElem = info.parentNode.querySelector("a[href][title]");
  421. if (linkElem)
  422. userLink = linkElem.href;
  423. }
  424. }
  425. return userLink;
  426. }
  427. }
  428.  
  429. function getFavKey(doc) {
  430. const columnPage = doc.getElementById("columnpage");
  431. const navbar = columnPage.querySelector('div[class*="favorite-nav"');
  432. const buttons = navbar.querySelectorAll('a[class*="button"][href]');
  433. let favButton;
  434. for (const button of buttons) {
  435. if (button.textContent.toLowerCase().includes("fav"))
  436. favButton = button;
  437. }
  438.  
  439. if (favButton) {
  440. const favKey = favButton.href.split("?key=")[1];
  441. const isFav = !favButton.href.toLowerCase().includes("unfav");
  442. return { favKey, isFav };
  443. }
  444. }
  445.  
  446. let isShowing = false;
  447. let notClosingElemsArr = [];
  448. let embeddedImage;
  449.  
  450. addEmbedded();
  451. window.updateEmbedded = addEmbedded;
  452.  
  453. document.addEventListener("click", (event) => {
  454. if (event.target.parentNode instanceof HTMLDocument && embeddedImage)
  455. embeddedImage.remove();
  456. });
  457.  
  458. async function addEmbedded() {
  459. for (const figure of document.querySelectorAll('figure:not([embedded])')) {
  460. figure.setAttribute('embedded', true);
  461. figure.addEventListener("click", function (event) {
  462. if (!event.ctrlKey && !event.target.id.includes("favbutton") && event.target.type != "checkbox") {
  463. if (event.target.href)
  464. return;
  465. else
  466. event.preventDefault();
  467. if (!isShowing)
  468. showImage(figure);
  469. }
  470. });
  471. }
  472. }
  473.  
  474. async function showImage(figure) {
  475. isShowing = true;
  476. embeddedImage = new EmbeddedImage(figure);
  477. embeddedImage.onRemove(() => {
  478. embeddedImage = null;
  479. isShowing = false;
  480. });
  481. }
  482.  
  483. function downloadImage() {
  484. console.log("Embedded Image Viewer downloading Image...");
  485. let url = window.location.toString();
  486. if (url.includes("?")) {
  487. const parts = url.split('?');
  488. url = parts[0];
  489. }
  490. const download = document.createElement('a');
  491. download.href = url;
  492. download.download = url.substring(url.lastIndexOf("/") + 1);
  493. download.style.display = 'none';
  494. document.body.appendChild(download);
  495. download.click();
  496. document.body.removeChild(download);
  497.  
  498. window.close();
  499. }
  500.  
  501. function trimEnd(string, toRemove) {
  502. if (string.endsWith(toRemove))
  503. string = string.slice(0, -1);
  504. return string;
  505. }