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