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