Retro Wiki Screenshot Viewer

A screenshot viewer for the Retro wikis.

目前为 2023-08-20 提交的版本。查看 最新版本

// ==UserScript==
// @name        Retro Wiki Screenshot Viewer
// @version     1.0.0
// @description A screenshot viewer for the Retro wikis.
// @author      J.C. Fields
// @namespace   jcfields.dev
// @homepageURL https://gitlab.com/jcfields/user-scripts
// @icon        https://jcfields.gitlab.io/user-scripts/org.segaretro.screenshots/icon.png
// @match       https://*.segaretro.org/*
// @match       https://*.necretro.org/*
// @match       https://info.sonicretro.org/*
// @license     MIT
// ==/UserScript==

"use strict";

const LOADING_IMAGE =
`iVBORw0KGgoAAAANSUhEUgAAACAAAAAoCAMAAACo9wirAAAACGFjVEwAAAACAAAAAPONk3AAAAAtUEx
URQAAADA5oQAAAH+E4Fpgwf7CoaMUAP39/cKAWqGhoaGl/VpaWv8mAMHBwVsGADRdwA0AAAABdFJOUwB
A5thmAAAAGmZjVEwAAAAAAAAAIAAAACgAAAAAAAAAAAAeAGQAAPVXCRQAAAEqSURBVDjLrZLRkoQgDAS
TCAkhq///uZeA1t1K9OmmateHbgODAPxbysgL3kaenIKIrV0OAOI3x0itbvTh4F0YTv1jlFWAEOrW+zk
j2cbcwymUtGR02FKheIswuvRp9G8jSuLgEoa/7497yTC6EA2jiDwLbhT/R1iP6hSkUyJMYwpjoWwCYux
SpClpS3mrwWsloifhSsopvrjD46iN0wE0FA/zUsF//i4G4xVPGm+2eDC3FTtF5Gt/i8GtkdlZokGyhk9
loVjBua1c1ZgFKRqgmd75bmYiGjfCy7qwKPuuZgS4gZ8EJCPiJAhCGMZjQnAD3o33EQVejd0vwzRUc8H
j3EunPYB4jnBDVTnhsn88fNDhn1yXZUgHDyUMn3G7GaSfK3QcZnzmV3jID/TCC88xkfIlAAAAGmZjVEw
AAAABAAAAEAAAAAgAAAAQAAAAIAAeAGQAALJsbRcAAABFZmRBVAAAAAI4y1XNQQoAMQhD0TSLQFC8/3G
nylBaF8J/IIJr2biGdDzCCEtyrz0jOkACKlZD7GKfZKrlr4FknZovvBMfhkgB1UqcFLwAAAAbdEVYdFN
vZnR3YXJlAEFQTkcgQXNzZW1ibGVyIDIuN8Hj04gAAAAASUVORK5CYII=`;

const SCREENSHOT_QUERY = ".bobscreen a, .bobtransform a";
const FULL_IMAGE_QUERY = ".fullImageLink a";
const DEFAULT_RATIO = 4 / 3;
const IMAGE_WIDTH = "95%";
const IMAGE_HEIGHT = "90%";
const THUMBNAIL_HEIGHT = 80;
const Z_INDEX = "10000001";

window.addEventListener("load", function() {
  const overlay = new Overlay();
  overlay.createElements();
  overlay.findScreenshots();
  overlay.populateGallery();

  window.addEventListener("keyup", function(event) {
    if (overlay.visible) {
      const {key} = event;

      if (key == "Escape") {
        overlay.close();
      }

      if (key == "ArrowLeft") {
        event.preventDefault();
        overlay.loadPrevScreenshot();
      }

      if (key == "ArrowRight") {
        event.preventDefault();
        overlay.loadNextScreenshot();
      }
    }
  });
  window.addEventListener("click", function(event) {
    const element = event.target;

    if (element.closest(SCREENSHOT_QUERY)) {
      overlay.showImage(event, element.closest(SCREENSHOT_QUERY));
    }

    if (element.closest("#screenshotLightbox")) {
      overlay.close();
    }

    if (element.matches(".screenshotThumbnail")) {
      overlay.loadScreenshot(element.dataset.index);
    }

    if (element.matches("#loadPrevScreenshot")) {
      overlay.loadPrevScreenshot();
    }

    if (element.matches("#loadNextScreenshot")) {
      overlay.loadNextScreenshot();
    }
  });
  window.addEventListener("resize", function() {
    overlay.screenshot.resize();
  });
});

/*
 * Overlay prototype
 */

function Overlay() {
  this.element = null;
  this.screenshot = null;
  this.gallery = null;
  this.link = null;
  this.prevButton = null;
  this.nextButton = null;

  this.index = 0;
  this.allLinks = [];

  this.visible = false;
}

Overlay.prototype.createElements = function() {
  const overlay = document.createElement("div");
  overlay.id = "screenshotOverlay";
  overlay.style.background = "rgba(0, 0, 0, 0.5)";
  overlay.style.color = "inherit";
  overlay.style.display = "flex";
  overlay.style.flexDirection = "column";
  overlay.style.height = "100%";
  overlay.style.justifyContent = "space-between";
  overlay.style.left = "0";
  overlay.style.position = "fixed";
  overlay.style.textAlign = "center";
  overlay.style.top = "0";
  overlay.style.visibility = "hidden";
  overlay.style.width = "100%";
  overlay.style.zIndex = Z_INDEX;

  const screenshotLightbox = document.createElement("div");
  screenshotLightbox.id = "screenshotLightbox";
  screenshotLightbox.style.alignItems = "center";
  screenshotLightbox.style.display = "flex";
  screenshotLightbox.style.flexGrow = "1";
  screenshotLightbox.style.justifyContent = "center";

  const screenshotImg = document.createElement("img");
  screenshotImg.id = "screenshotImage";
  screenshotImg.style.border = "1px solid #000";
  screenshotImg.style.boxShadow = "8px 8px 8px #000";
  screenshotImg.style.imageRendering = "pixelated";
  screenshotImg.style.visibility = "hidden";

  const loadingImg = document.createElement("img");
  loadingImg.id = "loadingImage";
  loadingImg.src = "data:image/png;base64," + LOADING_IMAGE;
  loadingImg.style.imageRendering = "pixelated";
  loadingImg.style.left = "50%";
  loadingImg.style.position = "absolute";
  loadingImg.style.top = "calc(50% - 1rem)";
  loadingImg.style.transform = "scale(3) translate(-50%, -50%)";
  loadingImg.style.visibility = "hidden";

  const screenshotGallery = document.createElement("div");
  screenshotGallery.id = "screenshotGallery";
  screenshotGallery.style.background = "rgba(0, 0, 0, 0.5)";
  screenshotGallery.style.display = "flex";
  screenshotGallery.style.flexShrink = "0";
  screenshotGallery.style.gap = "1em";
  screenshotGallery.style.overflowX = "auto";
  screenshotGallery.style.padding = "1em";

  const linkDiv = document.createElement("div");
  linkDiv.style.alignItems = "center";
  linkDiv.style.background = "rgba(0, 0, 0, 0.75)";
  linkDiv.style.color = "#fff";
  linkDiv.style.display = "flex";
  linkDiv.style.flexShrink = "0";
  linkDiv.style.fontSize = "1.5rem";
  linkDiv.style.justifyContent = "space-between";

  const a = document.createElement("a");
  a.id = "screenshotLink";
  a.style.background = "none";
  a.style.color = "#fff";
  a.style.gap = "1em";
  a.style.overflow = "hidden";
  a.style.textDecoration = "underline";
  a.style.textOverflow = "ellipsis";
  a.style.textShadow = "1px 1px #000";
  a.style.whiteSpace = "nowrap";

  const prevButton = document.createElement("button");
  prevButton.id = "loadPrevScreenshot";
  prevButton.textContent = "⬅️";
  prevButton.style.background = "none";
  prevButton.style.color = "inherit";
  prevButton.style.fontSize = "inherit";
  prevButton.style.margin = "0";
  prevButton.style.padding = "0.25em 0.5em";

  const nextButton = document.createElement("button");
  nextButton.id = "loadNextScreenshot";
  nextButton.textContent = "➡️";
  nextButton.style.background = "none";
  nextButton.style.color = "inherit";
  nextButton.style.fontSize = "inherit";
  nextButton.style.margin = "0";
  nextButton.style.padding = "0.25em 0.5em";

  screenshotLightbox.append(screenshotImg, loadingImg);
  linkDiv.append(prevButton, a, nextButton);
  overlay.append(screenshotLightbox, screenshotGallery, linkDiv);
  document.querySelector("body").append(overlay);

  this.element = overlay;
  this.screenshot = new Screenshot(screenshotImg, loadingImg);
  this.gallery = screenshotGallery;
  this.link = a;
  this.prevButton = prevButton;
  this.nextButton = nextButton;
};

Overlay.prototype.populateGallery = function() {
  for (const [i, link] of this.allLinks.entries()) {
    const imgSize = link.querySelector("img").getBoundingClientRect();
    const ratio = imgSize.width / imgSize.height;

    const thumbnail = new Image();
    thumbnail.className = "screenshotThumbnail";
    thumbnail.src = link.querySelector("img").src;
    thumbnail.width = ratio * THUMBNAIL_HEIGHT;
    thumbnail.height = THUMBNAIL_HEIGHT;
    thumbnail.style.aspectRatio = ratio;
    thumbnail.style.cursor = "pointer";
    thumbnail.style.imageRendering = "pixelated";
    thumbnail.dataset.index = i;

		this.gallery.append(thumbnail);
	}
};

Overlay.prototype.showImage = function(event, link) {
  // opens image normally if any modifier key is held
  if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
    return;
  }

  const index = this.allLinks.findIndex(function(compareLink) {
    return link == compareLink;
  });

  // only opens links that were filtered into the links array
  if (index < 0) {
    return;
  }

  event.preventDefault();

  this.setIndex(index);
  this.open(); // opens overlay before fetching anything so loading image shows
  this.loadImage(link);
};

Overlay.prototype.loadImage = function(link) {
  this.screenshot.loading();

  window.fetch(link.href).then(function(response) {
    return response.text();
  }).then(function(text) {
    // gets link to full image from file page
    const parser = new DOMParser();
    const htmlDocument = parser.parseFromString(text, "text/html");
    const rootElement = htmlDocument.documentElement;
    const fullImageLink = rootElement.querySelector(FULL_IMAGE_QUERY).href;

    return fullImageLink;
  }).catch(function() {
    console.error("Could not load file page:", link.href);
  }).then(function(fullImageLink) {
    // preserves aspect ratio of original image
    const imgSize = link.querySelector("img").getBoundingClientRect();
    const ratio = imgSize.width / imgSize.height;

    this.screenshot.change(fullImageLink, ratio);
    this.setLink(link.href);
  }.bind(this));
};

Overlay.prototype.open = function() {
  this.element.style.visibility = "visible";
  this.visible = true;
};

Overlay.prototype.close = function() {
  this.element.style.visibility = "hidden";
  this.screenshot.hide();
  this.visible = false;
};

Overlay.prototype.findScreenshots = function() {
  const links = document.querySelectorAll(SCREENSHOT_QUERY);
  this.allLinks = Array.from(links).filter(function(link) {
    // skips screenshots without links to a file page
    return link.href != undefined && /\/File:/.test(link.href);
  });
};

Overlay.prototype.loadScreenshot = function(index) {
  this.setIndex(index);
  this.loadImage(this.allLinks[this.index]);
};

Overlay.prototype.loadPrevScreenshot = function() {
  if (this.index <= 0) {
    return;
  }

  this.setIndex(this.index - 1);
  this.loadImage(this.allLinks[this.index]);
};

Overlay.prototype.loadNextScreenshot = function() {
  if (this.index >= this.allLinks.length - 1) {
    return;
  }

  this.setIndex(this.index + 1);
  this.loadImage(this.allLinks[this.index]);
};

Overlay.prototype.setIndex = function(index) {
  this.index = Math.max(0, Math.min(index, this.allLinks.length - 1));

  this.prevButton.disabled = this.index <= 0;
  this.nextButton.disabled = this.index >= this.allLinks.length - 1;

  const thumbnails = document.querySelectorAll(".screenshotThumbnail");

  // highlights active thumbnail
  for (const [i, thumbnail] of Array.from(thumbnails).entries()) {
    if (this.index == i) {
      thumbnail.style.boxShadow = "0 0 16px #fff";
      thumbnail.scrollIntoView();
    } else {
      thumbnail.style.boxShadow = "";
    }
  }
};

Overlay.prototype.setLink = function(href) {
  this.link.href = href;
  this.link.textContent = href.split("/").pop().replaceAll("_", " ");
};

/*
 * Screenshot prototype
 */
function Screenshot(screenshotImg, loadingImg) {
  this.screenshotImg = screenshotImg;
  this.loadingImg = loadingImg;

  this.ratio = DEFAULT_RATIO;
}

Screenshot.prototype.hide = function() {
  this.screenshotImg.style.visibility = "hidden";
  this.loadingImg.style.visibility = "hidden";
};

Screenshot.prototype.loading = function() {
  this.screenshotImg.style.visibility = "hidden";
  this.loadingImg.style.visibility = "visible";
};

Screenshot.prototype.change = function(src, ratio) {
  const image = new Image();
  image.src = src;
  image.addEventListener("load", function() {
    this.screenshotImg.src = image.src;
    this.ratio = ratio || DEFAULT_RATIO;
    this.resize();

    this.screenshotImg.style.visibility = "visible";
    this.loadingImg.style.visibility = "hidden";
  }.bind(this));
};

Screenshot.prototype.resize = function() {
  let width = "", height = "";

  // window is wider than tall
  if (window.innerWidth > this.ratio * window.innerHeight) {
    width  = "auto";
    height = IMAGE_HEIGHT;
  } else { // window is taller than wide
    width  = IMAGE_WIDTH;
    height = "auto";
  }

  this.screenshotImg.style.aspectRatio = this.ratio;
  this.screenshotImg.style.width  = width;
  this.screenshotImg.style.height = height;
};

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址