FA Embedded Image Viewer

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

当前为 2024-06-30 提交的版本,查看 最新版本

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